@dbx-tools/shared-core 0.3.43 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Browser-safe networking helpers built around {@link urlBuilder}: a
3
+ * tolerant `URL` coercion + chainable builder that gracefully handles
4
+ * partial inputs (bare hosts, path-only strings, `{ url }` wrappers),
5
+ * a small IPv4 / IPv6 address + CIDR toolkit (parsing and membership
6
+ * lookups), and email-address parsing ({@link parseEmails} /
7
+ * {@link isEmail}) that normalizes the CSV / single-string / array
8
+ * inputs email recipient and allow-list fields accept into a clean
9
+ * `string[]`. No node-only imports, so this module is the canonical
10
+ * home for anything URL-, IP-, or email-shaped that also has to run in
11
+ * a Vite / Webpack / esbuild client bundle. Node-only network helpers
12
+ * (e.g. DNS resolution) belong in a node-tagged package, not here.
13
+ *
14
+ * @module
15
+ */
16
+ import type { NonFunctionKeys } from "./object.js";
17
+ /**
18
+ * Anything {@link urlBuilder} (and {@link pathMatch}) know how to coerce
19
+ * into a URL:
20
+ *
21
+ * - A string: a host, a full URL, or a path / query / hash fragment.
22
+ * - A WHATWG `URL` instance.
23
+ * - Any object with a `url` field (e.g. a fetch `Request`, a Databricks
24
+ * `WorkspaceClient` config).
25
+ */
26
+ export type UrlLike = string | URL | {
27
+ url: string;
28
+ };
29
+ /** IP protocol family: `4` for IPv4, `6` for IPv6. */
30
+ export type IpVersion = 4 | 6;
31
+ /**
32
+ * A parsed IP address. `value` is the address as an unsigned integer
33
+ * (a 32-bit range for v4, 128-bit for v6) held in a `bigint` so both
34
+ * families share one comparison / masking path. Produce one with
35
+ * {@link parseIp}.
36
+ */
37
+ export interface ParsedIp {
38
+ version: IpVersion;
39
+ /** The address as an unsigned integer (`bigint` for uniform v4/v6 math). */
40
+ value: bigint;
41
+ }
42
+ /**
43
+ * A parsed CIDR block (`10.0.0.0/8`, `2001:db8::/32`). `base` is the
44
+ * network address with all host bits cleared, so membership is a
45
+ * single masked compare (see {@link ipInCidr}). Produce one with
46
+ * {@link parseCidr}. Carries the normalized `cidr` string for logging
47
+ * and to key back to caller-side metadata.
48
+ */
49
+ export interface Cidr {
50
+ version: IpVersion;
51
+ /** Network address (host bits zeroed) as an unsigned `bigint`. */
52
+ base: bigint;
53
+ /** Prefix length in bits (the number after the `/`). */
54
+ prefix: number;
55
+ /** Normalized `<address>/<prefix>` string. */
56
+ cidr: string;
57
+ }
58
+ /** Settable, non-method `URL` properties - the keys {@link UrlBuilder.with} accepts. */
59
+ type UrlPropertyKey = NonFunctionKeys<UrlBuilderImpl>;
60
+ /**
61
+ * A `URL` subclass with chainable, copy-on-write helpers. Every mutating
62
+ * method returns a fresh builder rather than editing in place, so a base
63
+ * builder can be safely reused. Construct one via {@link urlBuilder}.
64
+ */
65
+ declare class UrlBuilderImpl extends URL {
66
+ constructor(url: URL);
67
+ /** The scheme without the trailing colon (`https`, not `https:`). */
68
+ get scheme(): string;
69
+ set scheme(value: string);
70
+ /**
71
+ * Return a copy with a single `URL` property (`pathname`, `search`,
72
+ * `hostname`, `scheme`, ...) set to `value`, leaving this builder
73
+ * untouched.
74
+ */
75
+ with<K extends UrlPropertyKey>(key: K, value: UrlBuilderImpl[K]): UrlBuilder;
76
+ /**
77
+ * Return a copy with `pathSegments` appended to the current pathname.
78
+ * Segments may be strings or string arrays; each is trimmed of
79
+ * boundary slashes and blanks are dropped before joining with `/`.
80
+ */
81
+ withPathAppend(...pathSegments: (string | string[])[]): UrlBuilder;
82
+ /**
83
+ * Return a copy whose pathname is `pathSegments` joined with `/`,
84
+ * replacing any existing path. Segments may be strings or string
85
+ * arrays; each is trimmed of boundary slashes and blanks are dropped.
86
+ */
87
+ withPathReplace(...pathSegments: (string | string[])[]): UrlBuilder;
88
+ /**
89
+ * Test whether this URL's pathname is `path` or lives beneath it,
90
+ * matching on segment boundaries so `/api` matches `/api` and
91
+ * `/api/cool` but not `/apicool`. A missing leading slash on `path`
92
+ * is tolerated; query and hash are ignored (they aren't part of
93
+ * `pathname`). Matching is exact otherwise - trailing slashes are
94
+ * not normalized and `/` matches only the root.
95
+ */
96
+ pathMatches(path: string): boolean;
97
+ }
98
+ /** Public type for the {@link urlBuilder} return value. */
99
+ export type UrlBuilder = UrlBuilderImpl;
100
+ /** With no argument, resolves to the base origin (never `null`). */
101
+ export declare function urlBuilder(): UrlBuilder;
102
+ /**
103
+ * Coerce a {@link UrlLike} into a chainable {@link UrlBuilder}, or `null`
104
+ * when the input cannot be parsed into a URL. Never throws.
105
+ *
106
+ * - A `URL` instance or `{ url }` wrapper is adopted as-is.
107
+ * - A bare hostname (`"example.com"`) is upgraded to `https://`; an
108
+ * explicit scheme is preserved.
109
+ * - A path / query / hash fragment (`"/api"`, `"?q=1"`, `"#x"`) is
110
+ * resolved against {@link defaultUrl} (the browser origin, else
111
+ * `http://localhost`).
112
+ * - An empty / blank string or omitted input resolves to the base
113
+ * origin.
114
+ *
115
+ * @example
116
+ * urlBuilder("example.com"); // https://example.com/
117
+ * urlBuilder("http://x/path"); // http://x/path
118
+ * urlBuilder("/api/v2"); // http://localhost/api/v2
119
+ * urlBuilder({ url: "http://y" }); // http://y/
120
+ * urlBuilder(); // http://localhost/
121
+ */
122
+ export declare function urlBuilder(input?: UrlLike): UrlBuilder | undefined;
123
+ /**
124
+ * Convenience wrapper over {@link UrlBuilder.pathMatches}: coerce
125
+ * `input` via {@link urlBuilder} and test its pathname against `path`.
126
+ * Returns `false` for input that can't be parsed into a URL.
127
+ *
128
+ * @example
129
+ * pathMatch("/api/cool?q=1", "/api"); // true
130
+ * pathMatch("/apicool", "/api"); // false
131
+ * pathMatch("https://host/api", "/api"); // true
132
+ * pathMatch(request, "/api/v2"); // fetch Request
133
+ */
134
+ export declare function pathMatch(input: UrlLike, path: string): boolean;
135
+ /** Options for {@link parseEmails}. */
136
+ export interface ParseEmailsOptions {
137
+ /** Lower-case every address (useful for allow-lists / matching). Off by default. */
138
+ lowercase?: boolean;
139
+ /**
140
+ * Drop later case-insensitive duplicates, keeping first-seen casing.
141
+ * On by default - a recipient or pattern list rarely wants repeats.
142
+ */
143
+ dedupe?: boolean;
144
+ }
145
+ /**
146
+ * Normalize the shapes an email address field accepts - a single string,
147
+ * a comma / semicolon / whitespace separated list, an array of either,
148
+ * or nothing - into a clean `string[]`. Each entry is split on the list
149
+ * separators, trimmed, and de-emptied; blanks and non-string array
150
+ * members are dropped. By default duplicates are removed
151
+ * case-insensitively (first-seen casing wins) and casing is otherwise
152
+ * preserved; pass `lowercase` to fold everything to lower case (e.g. for
153
+ * an allow-list). This is the one place recipient lists, CC/BCC inputs,
154
+ * and sender allow-lists across the repo agree on how free-text email
155
+ * input is read. Never throws; does not validate - pair with
156
+ * {@link isEmail} when you need to reject malformed addresses.
157
+ *
158
+ * @example
159
+ * parseEmails("a@x.com, b@y.com; a@x.com"); // ["a@x.com", "b@y.com"]
160
+ * parseEmails(["A@x.com", " b@y.com "]); // ["A@x.com", "b@y.com"]
161
+ * parseEmails("*@corp.com", { lowercase: true }); // ["*@corp.com"]
162
+ * parseEmails(undefined); // []
163
+ */
164
+ export declare function parseEmails(input: string | readonly string[] | null | undefined, options?: ParseEmailsOptions): string[];
165
+ /**
166
+ * Pragmatic `local@domain.tld` shape check: non-empty local and domain
167
+ * parts around a single `@`, with a dotted domain and no whitespace.
168
+ * Deliberately loose (not full RFC 5322) - enough to catch obvious typos
169
+ * and give CLI / form feedback, not to gate delivery. Trims first. This
170
+ * is a purely syntactic test, so a wildcard allow-list pattern like
171
+ * `*@example.com` passes; distinguishing a pattern from a real address
172
+ * is the sender policy's job, not this validator's.
173
+ *
174
+ * @example
175
+ * isEmail("alice@example.com"); // true
176
+ * isEmail("nope"); // false (no @)
177
+ * isEmail("no@domain"); // false (no dotted domain)
178
+ */
179
+ export declare function isEmail(value: string): boolean;
180
+ /**
181
+ * Parse an IPv4 (`"1.2.3.4"`) or IPv6 (`"2001:db8::1"`,
182
+ * `"::ffff:1.2.3.4"`, `"::1"`) address into a {@link ParsedIp}, or
183
+ * `null` when the input isn't a valid literal. Never throws. Surrounding
184
+ * whitespace, `[...]` brackets around an IPv6 literal, and an IPv6 zone
185
+ * id (`fe80::1%eth0`) are all tolerated. The `value` is returned as a
186
+ * `bigint` so v4 and v6 share one comparison path.
187
+ *
188
+ * @example
189
+ * parseIp("10.0.0.1"); // { version: 4, value: 167772161n }
190
+ * parseIp("2001:db8::1"); // { version: 6, value: ... }
191
+ * parseIp("not-an-ip"); // null
192
+ */
193
+ export declare function parseIp(input: string): ParsedIp | null;
194
+ /**
195
+ * Parse a CIDR block (`"10.0.0.0/8"`, `"2001:db8::/32"`) into a
196
+ * {@link Cidr} with host bits cleared from `base`, or `null` when the
197
+ * input isn't a valid block (bad address, missing / out-of-range
198
+ * prefix). Never throws.
199
+ *
200
+ * @example
201
+ * parseCidr("10.0.0.0/8")?.base; // 167772160n (10.0.0.0)
202
+ * parseCidr("10.1.2.3/8")?.base; // 167772160n (host bits dropped)
203
+ * parseCidr("10.0.0.0/40"); // null (prefix > 32 for v4)
204
+ */
205
+ export declare function parseCidr(input: string): Cidr | null;
206
+ /**
207
+ * Test whether `ip` falls inside `cidr`. Both arguments accept either
208
+ * a string (parsed on the fly via {@link parseIp} / {@link parseCidr})
209
+ * or an already-parsed value - pass parsed values in hot loops to skip
210
+ * re-parsing. Returns `false` for unparseable input or a version
211
+ * mismatch (an IPv4 address is never inside an IPv6 block).
212
+ *
213
+ * @example
214
+ * ipInCidr("10.1.2.3", "10.0.0.0/8"); // true
215
+ * ipInCidr("11.0.0.1", "10.0.0.0/8"); // false
216
+ */
217
+ export declare function ipInCidr(ip: string | ParsedIp, cidr: string | Cidr): boolean;
218
+ /**
219
+ * Return the first CIDR in `cidrs` that contains `ip`, or `null` when
220
+ * none match. `cidrs` holds pre-parsed {@link Cidr} values (or any
221
+ * object extending it), so callers can carry side metadata on each
222
+ * entry (e.g. a region tag) and read it straight off the returned
223
+ * match. Linear scan; parse the address once by passing a
224
+ * {@link ParsedIp}.
225
+ *
226
+ * @example
227
+ * const ranges = ["10.0.0.0/8", "192.168.0.0/16"]
228
+ * .map(parseCidr)
229
+ * .filter((c): c is Cidr => c !== null);
230
+ * findContainingCidr("10.1.2.3", ranges)?.cidr; // "10.0.0.0/8"
231
+ */
232
+ export declare function findContainingCidr<T extends Cidr>(ip: string | ParsedIp, cidrs: Iterable<T>): T | null;
233
+ /**
234
+ * Whether `input`'s host is a loopback address - `localhost`, an IPv4
235
+ * `127.0.0.0/8` address, or IPv6 `::1` - i.e. a service running on this
236
+ * machine (e.g. a local registry). Accepts anything {@link urlBuilder}
237
+ * coerces (a URL string, `URL`, or `{ url }`); returns `false` when the
238
+ * input has no resolvable host.
239
+ *
240
+ * @example
241
+ * isLoopbackHost("http://localhost:4873"); // true
242
+ * isLoopbackHost("http://127.0.0.1"); // true
243
+ * isLoopbackHost("https://registry.npmjs.org"); // false
244
+ */
245
+ export declare function isLoopbackHost(input: UrlLike): boolean;
246
+ export {};