/** * Browser-safe networking helpers built around {@link urlBuilder}: a * tolerant `URL` coercion + chainable builder that gracefully handles * partial inputs (bare hosts, path-only strings, `{ url }` wrappers), * a small IPv4 / IPv6 address + CIDR toolkit (parsing and membership * lookups), and email-address parsing ({@link parseEmails} / * {@link isEmail}) that normalizes the CSV / single-string / array * inputs email recipient and allow-list fields accept into a clean * `string[]`. No node-only imports, so this module is the canonical * home for anything URL-, IP-, or email-shaped that also has to run in * a Vite / Webpack / esbuild client bundle. Node-only network helpers * (e.g. DNS resolution) belong in a node-tagged package, not here. * Reuse this module for URL, IP/CIDR, and email parsing instead of introducing * package-local normalizers with subtly different rules. * * @module */ import type { NonFunctionKeys } from "./object.ts"; /** Highest valid TCP port number. */ export declare const MAX_TCP_PORT = 65535; /** * Anything {@link urlBuilder} knows how to coerce * into a URL: * * - A string: a host, a full URL, or a path / query / hash fragment. * - A WHATWG `URL` instance. * - Any object with a `url` field (e.g. a fetch `Request`, a Databricks * `WorkspaceClient` config). */ export type UrlLike = string | URL | { url: string; }; /** IP protocol family: `4` for IPv4, `6` for IPv6. */ export type IpVersion = 4 | 6; /** * A parsed IP address. `value` is the address as an unsigned integer * (a 32-bit range for v4, 128-bit for v6) held in a `bigint` so both * families share one comparison / masking path. Produce one with * {@link parseIp}. */ export interface ParsedIp { version: IpVersion; /** The address as an unsigned integer (`bigint` for uniform v4/v6 math). */ value: bigint; } /** * A parsed CIDR block (`10.0.0.0/8`, `2001:db8::/32`). `base` is the * network address with all host bits cleared, so membership is a * single masked compare (see {@link ipInCidr}). Produce one with * {@link parseCidr}. Carries the normalized `cidr` string for logging * and to key back to caller-side metadata. */ export interface Cidr { version: IpVersion; /** Network address (host bits zeroed) as an unsigned `bigint`. */ base: bigint; /** Prefix length in bits (the number after the `/`). */ prefix: number; /** Normalized `
/` string. */ cidr: string; } /** Settable, non-method `URL` properties - the keys {@link UrlBuilder.with} accepts. */ type UrlPropertyKey = NonFunctionKeys; /** * A `URL` subclass with chainable, copy-on-write helpers. Every mutating * method returns a fresh builder rather than editing in place, so a base * builder can be safely reused. Construct one via {@link urlBuilder}. */ declare class UrlBuilderImpl extends URL { constructor(url: URL); /** The scheme without the trailing colon (`https`, not `https:`). */ get scheme(): string; set scheme(value: string); /** * Return a copy with a single `URL` property (`pathname`, `search`, * `hostname`, `scheme`, ...) set to `value`, leaving this builder * untouched. */ with(key: K, value: UrlBuilderImpl[K]): UrlBuilder; /** * Return a copy with `pathSegments` appended to the current pathname. * Segments may be strings or string arrays; each is trimmed of * boundary slashes and blanks are dropped before joining with `/`. */ withPathAppend(...pathSegments: (string | string[])[]): UrlBuilder; /** * Return a copy whose pathname is `pathSegments` joined with `/`, * replacing any existing path. Segments may be strings or string * arrays; each is trimmed of boundary slashes and blanks are dropped. */ withPathReplace(...pathSegments: (string | string[])[]): UrlBuilder; /** * Test whether this URL's pathname is `path` or lives beneath it, * matching on segment boundaries so `/api` matches `/api` and * `/api/cool` but not `/apicool`. A missing leading slash on `path` * is tolerated; query and hash are ignored (they aren't part of * `pathname`). Matching is exact otherwise - trailing slashes are * not normalized and `/` matches only the root. */ pathMatches(path: string): boolean; } /** Public type for the {@link urlBuilder} return value. */ export type UrlBuilder = UrlBuilderImpl; /** With no argument, resolves to the base origin (never `null`). */ export declare function urlBuilder(): UrlBuilder; /** * Coerce a {@link UrlLike} into a chainable {@link UrlBuilder}, or `null` * when the input cannot be parsed into a URL. Never throws. * * - A `URL` instance or `{ url }` wrapper is adopted as-is. * - A bare hostname (`"example.com"`) is upgraded to `https://`; an * explicit scheme is preserved. * - A path / query / hash fragment (`"/api"`, `"?q=1"`, `"#x"`) is * resolved against {@link defaultUrl} (the browser origin, else * `http://localhost`). * - An empty / blank string or omitted input resolves to the base * origin. * * @example * urlBuilder("example.com"); // https://example.com/ * urlBuilder("http://x/path"); // http://x/path * urlBuilder("/api/v2"); // http://localhost/api/v2 * urlBuilder({ url: "http://y" }); // http://y/ * urlBuilder(); // http://localhost/ */ export declare function urlBuilder(input?: UrlLike): UrlBuilder | undefined; /** Options for {@link parseEmails}. */ export interface ParseEmailsOptions { /** Lower-case every address (useful for allow-lists / matching). Off by default. */ lowercase?: boolean; /** * Drop later case-insensitive duplicates, keeping first-seen casing. * On by default - a recipient or pattern list rarely wants repeats. */ dedupe?: boolean; } /** * Normalize the shapes an email address field accepts - a single string, * a comma / semicolon / whitespace separated list, an array of either, * or nothing - into a clean `string[]`. Each entry is split on the list * separators, trimmed, and de-emptied; blanks and non-string array * members are dropped. By default duplicates are removed * case-insensitively (first-seen casing wins) and casing is otherwise * preserved; pass `lowercase` to fold everything to lower case (e.g. for * an allow-list). This is the one place recipient lists, CC/BCC inputs, * and sender allow-lists across the repo agree on how free-text email * input is read. Never throws; does not validate - pair with * {@link isEmail} when you need to reject malformed addresses. * * @example * parseEmails("a@x.com, b@y.com; a@x.com"); // ["a@x.com", "b@y.com"] * parseEmails(["A@x.com", " b@y.com "]); // ["A@x.com", "b@y.com"] * parseEmails("*@corp.com", { lowercase: true }); // ["*@corp.com"] * parseEmails(undefined); // [] */ export declare function parseEmails(input: string | readonly string[] | null | undefined, options?: ParseEmailsOptions): string[]; /** * Pragmatic `local@domain.tld` shape check: non-empty local and domain * parts around a single `@`, with a dotted domain and no whitespace. * Deliberately loose (not full RFC 5322) - enough to catch obvious typos * and give CLI / form feedback, not to gate delivery. Trims first. This * is a purely syntactic test, so a wildcard allow-list pattern like * `*@example.com` passes; distinguishing a pattern from a real address * is the sender policy's job, not this validator's. * * @example * isEmail("alice@example.com"); // true * isEmail("nope"); // false (no @) * isEmail("no@domain"); // false (no dotted domain) */ export declare function isEmail(value: string): boolean; /** * Parse an IPv4 (`"1.2.3.4"`) or IPv6 (`"2001:db8::1"`, * `"::ffff:1.2.3.4"`, `"::1"`) address into a {@link ParsedIp}, or * `null` when the input isn't a valid literal. Never throws. Surrounding * whitespace, `[...]` brackets around an IPv6 literal, and an IPv6 zone * id (`fe80::1%eth0`) are all tolerated. The `value` is returned as a * `bigint` so v4 and v6 share one comparison path. * * @example * parseIp("10.0.0.1"); // { version: 4, value: 167772161n } * parseIp("2001:db8::1"); // { version: 6, value: ... } * parseIp("not-an-ip"); // null */ export declare function parseIp(input: string): ParsedIp | null; /** * Parse a CIDR block (`"10.0.0.0/8"`, `"2001:db8::/32"`) into a * {@link Cidr} with host bits cleared from `base`, or `null` when the * input isn't a valid block (bad address, missing / out-of-range * prefix). Never throws. * * @example * parseCidr("10.0.0.0/8")?.base; // 167772160n (10.0.0.0) * parseCidr("10.1.2.3/8")?.base; // 167772160n (host bits dropped) * parseCidr("10.0.0.0/40"); // null (prefix > 32 for v4) */ export declare function parseCidr(input: string): Cidr | null; /** * Test whether `ip` falls inside `cidr`. Both arguments accept either * a string (parsed on the fly via {@link parseIp} / {@link parseCidr}) * or an already-parsed value - pass parsed values in hot loops to skip * re-parsing. Returns `false` for unparseable input or a version * mismatch (an IPv4 address is never inside an IPv6 block). * * @example * ipInCidr("10.1.2.3", "10.0.0.0/8"); // true * ipInCidr("11.0.0.1", "10.0.0.0/8"); // false */ export declare function ipInCidr(ip: string | ParsedIp, cidr: string | Cidr): boolean; /** * Return the first CIDR in `cidrs` that contains `ip`, or `null` when * none match. `cidrs` holds pre-parsed {@link Cidr} values (or any * object extending it), so callers can carry side metadata on each * entry (e.g. a region tag) and read it straight off the returned * match. Linear scan; parse the address once by passing a * {@link ParsedIp}. * * @example * const ranges = ["10.0.0.0/8", "192.168.0.0/16"] * .map(parseCidr) * .filter((c): c is Cidr => c !== null); * findContainingCidr("10.1.2.3", ranges)?.cidr; // "10.0.0.0/8" */ export declare function findContainingCidr(ip: string | ParsedIp, cidrs: Iterable): T | null; /** * Whether `input`'s host is a loopback address - `localhost`, an IPv4 * `127.0.0.0/8` address, or IPv6 `::1` - i.e. a service running on this * machine (e.g. a local registry). Accepts anything {@link urlBuilder} * coerces (a URL string, `URL`, or `{ url }`); returns `false` when the * input has no resolvable host. * * @example * isLoopbackHost("http://localhost:4873"); // true * isLoopbackHost("http://127.0.0.1"); // true * isLoopbackHost("https://registry.npmjs.org"); // false */ export declare function isLoopbackHost(input: UrlLike): boolean; export {};