reserve 0.2.0

Check domain name availability across grouped extensions, straight from the registry
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# reserve

Find a domain name you can actually register.

`reserve` checks names across domain extensions grouped the way people think about them: by industry, by region, and by how widely they are used. Filter the list, sort it, page through it, and sweep it. Every answer comes from the registry itself, and a lookup that cannot be answered is reported as unknown rather than guessed.

Documentation: [reserve.devops.bd](https://reserve.devops.bd)

```console
$ reserve your-domain-name --tld com,net,io,dev,com.bd,net.bd

   DOMAIN                   STATUS     SOURCE      TIME  NOTE
-  your-domain-name.com     TAKEN      registry  1322ms
+  your-domain-name.com.bd  AVAILABLE  text       656ms
+  your-domain-name.dev     AVAILABLE  registry  1354ms
+  your-domain-name.io      AVAILABLE  registry  1286ms
-  your-domain-name.net     TAKEN      registry  1113ms
+  your-domain-name.net.bd  AVAILABLE  text       627ms

4 available  2 taken  0 unknown   (6 checked)
```

## Contents

- [Why it exists]#why-it-exists
- [Requirements]#requirements
- [Installation]#installation
- [Quick start]#quick-start
- [How a verdict is reached]#how-a-verdict-is-reached
- [Naming what to check]#naming-what-to-check
- [Choosing extensions]#choosing-extensions
- [Filtering]#filtering
- [Sorting and paging]#sorting-and-paging
- [The interactive picker]#the-interactive-picker
- [Seeing more about a name]#seeing-more-about-a-name
- [Saving results]#saving-results
- [Using your own server lists]#using-your-own-server-lists
- [Pacing and rate limits]#pacing-and-rate-limits
- [Reading a text answer]#reading-a-text-answer
- [On prices]#on-prices
- [Command reference]#command-reference
- [Exit codes]#exit-codes
- [Output and streams]#output-and-streams
- [Environment]#environment
- [Architecture]#architecture
- [Building from source]#building-from-source
- [The catalog]#the-catalog
- [Support and compatibility]#support-and-compatibility
- [Contributing]#contributing
- [Licence]#licence

## Why it exists

Most availability checkers present a flat list of extensions and a yes or no. Three things go wrong with that.

**Scale.** There are more than 1,400 delegated extensions. A flat list is unusable, and sweeping all of them wastes the registry's patience and yours. `reserve` groups them so you can ask for the twenty-five that matter to a software company, or the country zones for South Asia, without hand-writing a list.

**Coverage.** Bangladesh's `.bd` has no structured lookup service and IANA publishes no server for it, so most tools report it as unanswerable. `reserve` carries a verified server and answers every `.bd` zone at both the second and third level. The same applies to a number of other country zones whose endpoints are absent from IANA's records.

**Honesty.** Registries throttle bulk clients, and the older text protocol carries no status codes at all: a refusal arrives as ordinary prose down the same socket as a real answer. Tools that cannot tell the difference report a whole zone as free the moment they are throttled. `reserve` treats every unanswerable lookup as unknown, with the reason attached, and never as available.

## Requirements

- Rust 1.88 or newer, and a C toolchain for the TLS backend. Both are needed only to install, not to run.
- Network access outbound on TCP 443 for structured lookups and TCP 43 for the text protocol. A network that blocks 43 will still work; those zones report as unreachable rather than as available.
- A UTF-8 locale for the full terminal presentation. Outside one, the tool falls back to plain characters.

## Installation

```sh
cargo install reserve --locked
```

This builds from source. There are no prebuilt binaries yet.

Shell completions are generated on demand from the installed binary's own command definition, so they cannot drift from the real flags:

```sh
reserve completions zsh  > "${fpath[1]}/_reserve"
reserve completions bash > /etc/bash_completion.d/reserve
reserve completions fish > ~/.config/fish/completions/reserve.fish
```

Confirm the installation and see what the tool detects about its surroundings:

```sh
reserve doctor
```

## Quick start

```sh
# Check one name across a group of extensions
reserve your-domain-name --group tech

# Check specific extensions
reserve your-domain-name --tld com,net,io

# Check a full domain exactly as typed
reserve example.com.bd

# Check several names at once
reserve alpha bravo charlie --tld com

# Browse the catalog without checking anything
reserve extensions --group south-asia

# Open the interactive picker
reserve your-domain-name
```

## How a verdict is reached

Three sources are tried in order, and only the first two can prove a name is free.

1. **The registry's structured service.** Authoritative. A "no such object" answer is a fact, not an inference. Covers roughly 1,200 extensions.
2. **The registry's text service on port 43.** The only source that answers for many country extensions. `reserve` bundles a table of servers generated from IANA's own records, plus servers confirmed by live query that IANA does not publish.
3. **DNS.** Fast, and only ever proves a name is *in use*. A name absent from DNS may be registered and never delegated, so DNS alone never yields an "available" verdict.

Select one with `--source registry`, `--source text`, or `--source dns`. The default tries all three in order.

Four statuses are possible:

- `AVAILABLE` — a registry stated the name is unregistered.
- `TAKEN` — a registry returned a record, or DNS proved the name is in use.
- `UNKNOWN` — the lookup could not be answered. The reason is always attached: rate limited, declined, timed out, unreachable, or unreadable. This is never a synonym for available.
- Not publicly registrable — the extension exists but the public cannot register under it.

## Naming what to check

```sh
reserve example                      # a bare name, crossed with the chosen extensions
reserve example.com                  # a full domain, checked exactly as typed
reserve one two three --tld com,dev  # several names at once
reserve --names-from shortlist.txt   # read names from a file
reserve example --tlds-from zones.txt
```

A name carrying a dot is a domain you are asking about directly, so it is checked as given whatever extensions the run selected. A list file takes one entry per line; commas also separate, `#` starts a comment, blank lines are skipped, and duplicates are dropped.

A name typed as a person would speak it is converted to a form a registry accepts, and the change is reported rather than applied silently. `My Cool Startup!` becomes `my-cool-startup`. Names written in Bengali, Arabic, or any other script keep every mark their writer typed, because stripping punctuation from those scripts deletes characters that carry meaning and would check a different name.

## Choosing extensions

Extensions are grouped four ways. `reserve groups` lists them all.

- **By popularity**`top-10`, `top-25`, `top-50`, `top-100`, ranked by how many names are registered in each zone.
- **Hand-picked**`popular`, `classic`, `country-codes`, `domain-hacks`, `registrable`, `everything`.
- **By industry**`tech`, `shop`, `business`, `media`, `finance`, `health`, `education`, `food`, `travel`, `realestate`, `legal`, `crypto`, `creative`, `nonprofit`.
- **By region**`south-asia`, `southeast-asia`, `east-asia`, `europe`, `north-america`, `latin-america`, `africa`, `middle-east`, `oceania`.

Group membership is computed from extension metadata rather than stored as a list, so a group cannot drift out of step with the data behind it. Groups combine: ask for two and you get the union.

```sh
reserve example --group tech
reserve example -g tech -g creative
reserve example -g south-asia --depth second
```

## Filtering

Filters narrow whatever you selected, and they all combine.

- `--search <TEXT>` matches the extension, its country, and its industries.
- `--industry <KEY>` and `--region <KEY>` narrow to a sector or a part of the world.
- `--cctld` keeps only two-letter country extensions.
- `--depth second|third` separates plain extensions from multi-label ones such as `co.uk`.
- `--length 2`, `-3`, `4-`, or `2-4` filters by length, measured on the delegated label.
- `--exclude <EXT>` drops an extension from whatever else was chosen.
- `--include-restricted` adds zones with eligibility conditions, hidden by default.

Zones the public cannot register under are excluded from a sweep by default, so a free name in a zone nobody can buy is never offered as available.

## Sorting and paging

```sh
reserve extensions --sort popularity          # most registered first
reserve extensions --sort length --order asc  # shortest first
reserve extensions --sort name --order desc   # z to a
```

Each field runs the direction that reads best for it unless you say otherwise. Popularity counts down; length and name count up. An extension with no rank always sorts to the bottom whichever direction you choose, so reversing a sort never floats unknown data to the top.

Results are paged to whatever your terminal fits.

```sh
reserve extensions -g everything --page 3
reserve extensions -g everything --page-size 40
reserve extensions -g everything --all-pages
```

```console
$ reserve extensions --group tech --page-size 6

EXTENSION  RANK  KIND     USED FOR
com           1  generic  business, tech, shop
net           4  generic  tech, business
xyz           7  generic  tech, creative, crypto
co           21  country  Colombia · business, tech · often used off-…
site         30  generic  tech, creative
app          35  generic  tech

1-6 of 34 extensions · page 1 of 6 · sorted by popularity (most used first)
next: --page 2  |  --all-pages
```

## The interactive picker

Run `reserve` with a name and no extensions, at a terminal, and it opens a picker rather than guessing:

```
┌ Groups - active ─────────────┐┌ Extensions (3 chosen) ───────────────────┐
│ By popularity                ││   [x] .com  business, tech, shop         │
│ >     Top 10          3/10   ││   [x] .net  tech, business               │
│       Top 25          0/22   ││   [x] .xyz  tech, creative, crypto       │
│ Hand-picked                  ││   [ ] .top  business                     │
│       Popular         0/22   ││   [ ] .nl   Netherlands - business       │
└──────────────────────────────┘└──────────────────────────────────────────┘
Up/Down move  Tab pane  Space toggle  / search  a all  n none  o settings  Enter run  ? help
```

Key bindings:

- `Up`/`Down` or `k`/`j` — move through the list
- `Tab` — switch between the group pane and the extension pane
- `Space` — pick or unpick one extension, or a whole group
- `/` — search by extension, country, or industry
- `a` — pick everything currently shown; `n` — unpick everything
- `o` — open the settings screen
- `Enter` — run the sweep; `q` or `Esc` — leave without checking
- `?` — open a card listing every binding, including those the hint bar drops as the terminal narrows

The picker never opens where there is nobody to answer: piped input, `--no-input`, or naming any extension all skip it, so a script behaves the same every time. It also stays shut when every name given already carries its extension, since there is nothing left to select. `--interactive` forces it open.

### The settings screen

`o` opens a screen carrying every option that shapes a run, so nothing on the command line is out of reach of someone working at the terminal:

```
┌ Settings 1 of 31  -  Left/Right change   Esc back   Enter run ──────────┐
│> names to check                 example_                                │
│  read names from a file         no file                                 │
│  include restricted zones       no                                      │
│  country extensions only        no                                      │
│  extension depth                any                                     │
│  ...                                                                    │
└─────────────────────────────────────────────────────────────────────────┘
```

`Up`/`Down` move between rows, `Left`/`Right` or `Space` change the row under the cursor, a text row takes letters with `Backspace` to delete, `Esc` returns to the panes, and `Enter` starts the run from anywhere.

A row that narrows the extension list narrows it on screen immediately, so `country extensions only` visibly drops everything else from the pane behind it, and anything it hides also leaves the chosen set rather than being checked in silence. A number row refuses letters at the keystroke, so a value that could not be read never reaches the run.

Nothing in the picker can replace the report on screen. `save the files as JSON` changes only what is written to disk.

## Seeing more about a name

```sh
reserve example.com --details        # registrar, dates, status, nameservers, DNSSEC, abuse contact
reserve example.com --responder      # which server answered
reserve example.com --dns            # A, AAAA, NS, MX, TXT, SOA records
reserve example --where-to-buy       # registry page and prefilled registrar searches
reserve example.com --full           # every section at once
```

The where-to-buy links are prefilled searches in a fixed order. They are not an offer, and not a claim that a given registrar carries that extension.

## Saving results

```sh
reserve example -g tech --save          # example-available.txt, example-unavailable.txt, example-unknown.txt
reserve example -g tech --out results   # into a directory you name
reserve example -g tech --save --json   # the same three files as JSON arrays
reserve example -g tech --save --append # merge into what is already there
```

Saving never replaces what you see: the table and the tally are printed as usual and the files are written as well.

Each file is named after what you searched, so a folder of results still says which run produced what. Checking one name across forty extensions gives one set of files, because the extension varies and the search does not. Checking several names names the file after the first and counts the rest, as in `alpha-and-2-more-available.txt`. The full path is printed when the run ends.

Only letters and digits survive into the file name, in lower case, with anything else becoming a hyphen. A name is typed by a person and must never be able to steer the write out of the directory you chose. A name written in another script keeps its own letters, because stripping those would name the file after a different search.

Because the name forms part of the file name, `--append` merges a repeat of the same search rather than folding unrelated runs together. A different search writes its own files and leaves the earlier ones alone.

Files are written to a temporary file and renamed into place, so an interrupted run never leaves a half-written file. An unknown result always goes to its own file, so a lookup that could not be answered is never filed among the available ones. Result files are created readable by their owner only, because a full record carries the registrar and its abuse contact.

## Using your own server lists

```sh
reserve example --registry-servers mine.json                # structured-service list
reserve example --text-servers mine.json                    # text-service table
reserve example --text-servers mine.json --servers-replace  # replace rather than overlay
reserve example --no-referral                               # never ask IANA who serves an extension
```

By default a supplied list overlays the built-in one, so your entries win for the extensions they name and everything else keeps working. `--servers-replace` replaces it instead, which is how you reach an internal zone with no public entry at all.

When nothing already known answers, the tool asks IANA who serves that extension today, which repairs a built-in host that has gone stale. That lookup is cached per extension for the run, and `--no-referral` turns it off.

## Pacing and rate limits

A sweep across hundreds of extensions can hammer a small number of servers, because one operator often answers for hundreds of extensions from a single endpoint. `reserve` paces itself per registry host, never per extension: a per-extension limiter would multiply the real request rate by the number of extensions in the sweep and earn an immediate block.

- A global cap on lookups in flight.
- A per-registry token bucket and concurrency cap, seeded from each operator's published limit where one exists and a cautious default where none does.
- A breaker that stops asking a registry that has begun refusing, waits, then sends a single probe before resuming.
- Backoff drawn uniformly from zero up to a ceiling that doubles with each refusal, so requests that fail together do not retry together.
- A registry's own stated retry delay honoured as given, capped so a mistaken hint cannot stall a run.

Pass `--cautious` to hold every endpoint to the cautious limit. It acts as a ceiling: `--rate` and `--per-registry` can only lower it.

## Reading a text answer

The older protocol has no status codes. "Not registered", "you are asking too fast", and "I do not serve that extension" all arrive as ordinary prose down the same socket, which is why tools built on it report whole zones as free the moment they are throttled.

`reserve` gathers every signal in an answer and weighs them, rather than taking the first phrase that matches:

- A verdict is read from the record, not from the legal notice around it. Several registries close with wording such as "should you wish to contact the registrant", or state that their service is rate limited on every answer they serve. Neither is evidence about the name.
- Matching is negation-aware, because a registry that answers `Available` or `Not Available` would otherwise have every taken name in its zone reported as free.
- An answer about a different name is refused rather than applied to the name asked about.
- A name a registry states is blocked or reserved is reported as such, not as available and not as an error.
- A reply cut short after the registry has spoken is kept, so a throttled registry reads as rate limited rather than as an unreachable network.

## On prices

`reserve` deliberately shows no prices.

There is no single price for a domain extension. Comparing two of the most transparent registrars across the 405 extensions both of them price, the same extension differs by anywhere from 66 percent below to 39 percent above. Registry premium names sit outside every published list and can cost thousands. Published figures exclude taxes, which depend on where you are. Wholesale prices move on scheduled dates.

A price column would imply a precision that does not exist. Check the price at the registrar you intend to buy from.

## Command reference

```sh
reserve [NAME]...               # check names
reserve groups                  # every group, by family
reserve groups --family region  # one family
reserve extensions              # browse the catalog
reserve extensions --json       # machine-readable, for scripts
reserve config show             # resolved settings and where each came from
reserve config path             # where settings, cache, and data live
reserve doctor                  # version, catalog, terminal, paths, network
reserve completions <shell>     # write a completion script to stdout
```

Every subcommand offers a `--json` mode where a script would want one.

## Exit codes

Scripts branch on these. They are a contract and do not change meaning within a major version.

- `0` — the command succeeded; for a sweep, at least one name is available
- `1` — the sweep ran and nothing is available
- `2` — the arguments were wrong
- `4` — the network could not be reached
- `5` — a file could not be written
- `130` — the run was interrupted

Every failure also carries a stable identifier, printed as `code: group.unknown`, so a script can branch without parsing prose.

## Output and streams

Results go to stdout. Progress, warnings, prompts, and errors go to stderr, so a pipe receives only data. When stdout is not a terminal the decoration is dropped and the output stays parseable. A closed pipe ends the run quietly rather than panicking.

On a terminal, a run opens with its start time and closes with its duration, both on stderr:

```console
started 2026-08-20T00:34:22+06:00
+  your-domain-name.com.bd  AVAILABLE  text  803ms
finished 1 checked in 1.29s
```

The timestamp carries its offset, so a line pasted into an issue says which clock it came from.

A long sweep draws a one-line progress indicator on stderr, naming the domain in hand alongside the count:

```console
checking 14 of 22  your-domain-name.shop 1s
```

It never touches stdout. It stays away entirely when stderr is not a terminal, in continuous integration, under `--json` or `--no-input`, and whenever colour is suppressed.

When nothing at all could be answered and every failure was of one kind, the run says so and emits a code a script can branch on:

```console
network: no lookup reached a registry, so this machine may have no working connection
  code: network.unreachable
```

That is derived from the lookups the run already made, not from a separate connectivity check. A pre-flight probe races the real request and reports success behind a captive portal, so there is none.

Colour follows the usual conventions in order: an explicit `--color` flag wins, then `NO_COLOR`, then `FORCE_COLOR` and `CLICOLOR_FORCE`, then a dumb terminal, then whether the stream is a terminal at all. Colours come from the sixteen named terminal colours, so retheming your terminal rethemes the tool. Meaning is never carried by colour alone; a symbol or a word always carries it too.

## Environment

Every setting below is read at startup. A flag always beats an environment variable, which always beats the built-in default.

- `RESERVE_CONCURRENCY` — lookups in flight across the run, same as `-c`.
- `RESERVE_TIMEOUT` — seconds to wait on any single request, same as `--timeout`.
- `RESERVE_WIDTH` — lay the table out for this many columns, same as `--width`.
- `RESERVE_NO_INPUT` — never prompt and never open the picker, same as `--no-input`. Accepts `1`, `true`, `yes`, and `on`.
- `NO_COLOR`, `CLICOLOR=0` — suppress colour and the progress line.
- `CLICOLOR_FORCE`, `FORCE_COLOR` — keep colour through a pipe.
- `TERM``dumb`, empty, or unset on a Unix host means no colour and no progress line.
- `COLUMNS`, `LINES` — override the terminal size the layout is fitted to.
- `RUST_LOG` — widen what `-v` turned on, using the standard filter syntax.
- `RUST_BACKTRACE` — print a backtrace if the tool panics.

Everything the tool writes for itself lives under one directory named for the business that publishes it, `devops.bd/reserve`, inside your platform's own config, cache, and data directories. Run `reserve config path` to see the three resolved locations; deleting them removes every trace of the tool.

## Architecture

Two crates in one workspace, with a strict boundary between them.

- `crates/reserve-core` — all the logic, published as a library. It reads no process environment and touches no global streams, so a caller supplies its own context.
  - `tld/` — the extension catalog, groups, filters, and sorting
  - `lookup/` — the three sources, the verdict reader, and the sweep engine
  - `limit/` — per-registry pacing, the token bucket, and the breaker
  - `error.rs` — one error type carrying the user-facing message, the cause chain, and the exit class
- `crates/reserve-cli` — a thin binary named `reserve`. It builds a context, calls the core, and turns the result into an exit code.
  - `cli.rs` — the whole command surface, declared once
  - `plan.rs` — everything a run can be told to do, in a shape both the command line and the picker write into
  - `app.rs` — turning a plan into work, and work into an exit code
  - `tui.rs` — the interactive picker and its settings screen
  - `progress.rs`, `lifecycle.rs` — the progress line and the run's opening and closing lines
  - `output/` — the table, the detail sections, and the colour palette

Both bundled data files live inside the crate that embeds them, because a published crate cannot package a file from outside its own directory.

## Building from source

```sh
cargo build --release
cargo test --workspace
```

Rust 1.88 or newer. Man pages are generated at build time and completion scripts are written on demand, both from the same command definition the parser uses, so neither can drift from the real flags.

The full verification gate, which every change is held to:

```sh
cargo fmt --all -- --check
cargo check   --workspace --all-targets --all-features --locked
cargo clippy  --workspace --all-targets --all-features --locked -- -D warnings
cargo nextest run --workspace --all-features --locked
cargo test    --workspace --all-features --locked --doc
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked
cargo audit --deny warnings
cargo deny check
```

## The catalog

Both bundled datasets are generated rather than hand-edited, so every entry carries the same fields.

```sh
python3 tools/build_catalog.py         # extensions, groups, and ranks
python3 tools/build_whois_servers.py   # text-protocol servers, from IANA plus verified additions
```

Ranks are registration counts, taken from published industry figures and cross-checked against daily zone-file counts, preferring a registry's own published figure where one exists. A zone count is not the same as a registry's own figure, because a registered-but-undelegated name never appears in the zone. Country extensions that register at the third level, such as `.bd`, are undercounted by any zone walk, so their rank is left blank rather than stated wrongly.

Rank measures registrations, not usage. The two part company sharply: some heavily registered extensions are renewed by fewer than one registrant in six. Registration count is the right measure for a tool about registering a name, but a high rank is not evidence that a zone is well used.

One question decides what earns a place in the catalog: can anyone outside a closed body ever hold a name there. A government or military zone cannot, so it is left out. A zone that can be held once the right paperwork is supplied is carried and marked restricted, which keeps it out of a sweep by default without pretending it is unobtainable. Academic zones are the clearest case, since an institution obtains one by producing its accreditation. Pass `--include-restricted` to include them.

Leaving a zone out costs nothing for a direct lookup. A name under it is still checked exactly as typed, whether or not the extension is in the catalog.

## Support and compatibility

- The command surface is the public API: flags, subcommands, output formats, exit codes, environment variables, and the configuration schema. These follow semantic versioning.
- `reserve-core` is published as a library and follows the same versioning for its public types.
- The minimum supported Rust version is 1.88. Raising it is a minor version change.
- Bundled data is refreshed by regenerating it from source, not by editing it.

## Contributing

Issues and pull requests are welcome at [devops-infinity/reserve](https://github.com/devops-infinity/reserve). Security reports go through the process in [SECURITY.md](SECURITY.md) rather than a public issue.

## Licence

MIT. See [LICENSE](LICENSE).