Adds tools/Export-IpBlocklist.ps1, the producer half of the in-app blocklist
gate. It merges a thin, non-overlapping set of public IP threat feeds into one
de-duplicated, bogon-filtered list and writes the plain-text file the shard
reads via `blocklistFile`. Parsing is done in a compiled Add-Type hot loop
because the anchor feed alone is ~4M lines; the output is streamed so millions
of entries never become millions of strings.
The file is written to a .tmp sibling and swapped with File.Replace, so the
shard never observes a half-written list, and a total feed outage refuses to
overwrite a good list with an empty one. Re-running is idempotent: the script
exits without downloading anything while the list on disk is younger than
-MinInterval (default 2h, the anchor feed's own refresh period), so a
misconfigured scheduler cannot hammer the upstream feeds. Age comes from the
`generated=` header the script writes, falling back to mtime, so no sidecar
state file is needed. -Force overrides.
`blocklistFile` now defaults to Configuration/ip-blocklist.txt instead of being
empty. The gate stays inert while that file is absent, so this is a no-op for
shards that never run the generator, and dropping the file in later is picked
up by the existing poll with no restart.
Two fixes this exposed:
- FileBlocklist used the configured path verbatim despite documenting it as
relative to Core.BaseDirectory, so a relative path resolved against the
process working directory. Relative paths now resolve against BaseDirectory;
absolute paths are honored so several shards can share one generated list.
- A missing file is now the shipped default rather than a misconfiguration, so
boot logs "inert: no blocklist at ..." instead of "loaded 0 entries".
tools/ stays ignored except for this script.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>