escrow

module
v1.5.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 25, 2026 License: MIT

README ΒΆ

πŸ” escrow

A lightweight supply-chain proxy that enforces configurable trust policies before any package reaches your developers or CI pipeline. Covers 7 ecosystems in a single binary.

developer / CI  β†’  escrow  β†’  upstream registry
                      β”‚
                policy engine
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         age         osv      publisher  popularity

Packages that fail policy are blocked at the proxy level β€” they never reach the tool. Operators review blocked events in the real-time dashboard and approve packages with a single click.


πŸš€ Quick Install

brew tap jverhoeks/tap
brew install escrow

Run as a background service (auto-starts on login):

brew services start escrow
# β†’ http://localhost:7888/dashboard
# Dashboard credentials are in: $(brew --prefix)/var/log/escrow.log
brew services stop escrow      # stop
brew services restart escrow   # reload config

Config lives at $(brew --prefix)/etc/escrow/escrow.toml β€” edit it to enable more ecosystems, then restart the service.

🐳 Docker
docker run -p 7888:7888 ghcr.io/jverhoeks/escrow:latest

Debug config (all 7 ecosystems, full policy, admin / escrow):

cd docker/
mkdir -p data && cp escrow.debug.toml data/escrow.toml
docker compose up -d
# β†’ http://localhost:7888/dashboard
πŸ“¦ Binary
# macOS arm64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-darwin-arm64 -o escrow
chmod +x escrow && ./escrow

# macOS amd64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-darwin-amd64 -o escrow
chmod +x escrow && ./escrow

# Linux amd64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-linux-amd64 -o escrow
chmod +x escrow && ./escrow

On first boot escrow generates escrow.toml with a random dashboard password and prints credentials to stdout.

βš™οΈ Flags
./escrow                              # binds to 127.0.0.1:7888 (localhost only)
./escrow --host=0.0.0.0               # listen on all interfaces (team/CI use)
./escrow --config=/etc/escrow/escrow.toml
./escrow --host=0.0.0.0 escrow.toml # flag + positional config path

πŸ’‘ On first boot, credentials are printed to stdout. Save them β€” or find them in the generated escrow.toml.


🌐 Supported Ecosystems

Ecosystem Tools Proxy URL Config key
npm npm, pnpm, yarn, bun http://localhost:7888/ npm = true
PyPI pip, uv http://localhost:7888/pypi/simple/ pypi = true
Go modules go http://localhost:7888/go/ go = true
Cargo cargo http://localhost:7888/cargo/ cargo = true
Composer composer http://localhost:7888/composer/ composer = true
NuGet dotnet, nuget http://localhost:7888/nuget/index.json nuget = true
Maven / Gradle mvn, gradle http://localhost:7888/maven2/ maven = true

⚑ GitHub Actions

Use escrow as a one-step supply-chain gate in any CI pipeline. Add it before your install steps β€” no other changes needed:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - uses: jverhoeks/escrow@v1
        with:
          ecosystems: 'npm'
          min-days: '7'
          osv-severity: 'HIGH'

      - uses: actions/setup-node@v6
        with:
          node-version: '20'

      - run: npm install --ignore-scripts
      # npm automatically uses the escrow registry β€” no other changes needed

Escrow sets NPM_CONFIG_REGISTRY, PIP_INDEX_URL, GOPROXY, etc. automatically so every install command routes through the proxy. The package cache is stored in GitHub Actions cache and restored on every run β€” warm cache runs require zero upstream calls.

Input Default Description
ecosystems npm,pypi,go,cargo Comma-separated list to enable
min-days 7 Age gate threshold
osv-severity HIGH Minimum CVE severity to block (off to disable)
version v1.4.1 Escrow binary version
port 7888 Local proxy port
cache-key-suffix `` Append to cache key for manual busting

Output: proxy-url β€” the base URL (http://127.0.0.1:7888).

β†’ Full guide: docs/github-actions.md


πŸ“– Per-Tool Quickstarts

Step-by-step guides for global setup, per-project setup, verify, and remove for each tool:

Tool Guide
npm docs/quickstart/npm.md
pnpm docs/quickstart/pnpm.md
yarn docs/quickstart/yarn.md
bun docs/quickstart/bun.md
pip docs/quickstart/pip.md
uv docs/quickstart/uv.md
go docs/quickstart/go.md
cargo docs/quickstart/cargo.md
composer docs/quickstart/composer.md
dotnet / NuGet docs/quickstart/dotnet.md
maven docs/quickstart/maven.md
gradle docs/quickstart/gradle.md
GitHub Actions docs/github-actions.md

πŸ›‘οΈ What Escrow Protects Against

Threat Protected?
βœ… Same-day injection attacks (packages published and spread within hours) blocked by age gate
βœ… Packages with known CVEs (MEDIUM/HIGH/CRITICAL by default) blocked by OSV scan
βœ… Packages from brand-new publisher accounts flagged by publisher signal
βœ… Packages with sudden download spikes (possible hijacking) flagged by popularity signal
βœ… Packages on your explicit blocklist blocked at allowlist/blocklist check
βœ… Air-gap: packages that haven't been reviewed never reach developer machines proxy-level enforcement
❌ Postinstall hooks in packages that do pass the gate use ignore-scripts=true per tool
❌ Typosquatting on packages that pass age/vuln gates not yet implemented
❌ Git-protocol npm deps (npm install github:user/pkg) bypass the registry entirely
❌ Composer ZIP archives (artifact air-gap) metadata filtered; archives fetched from CDN
❌ Publisher signal for Go, Cargo, NuGet, Maven no public API equivalent

⚠️ Postinstall hooks are the most important gap. Escrow filters packages from manifests but does not strip postinstall hooks from packages that pass. Set ignore-scripts=true (npm/pnpm), enableScripts: false (yarn), or only-binary = [":all:"] (uv) on every developer machine.


πŸ”§ Point Your Tools at Escrow

npm / pnpm / yarn / bun
# .npmrc
registry=http://localhost:7888
ignore-scripts=true
pip / uv
# uv.toml
[pip]
index-url = "http://localhost:7888/pypi/simple/"
Go modules
export GOPROXY=http://localhost:7888/go,off   # use ,off not ,direct

⚠️ ,off vs ,direct: Using direct as fallback means Go silently downloads from the original VCS if escrow is unreachable β€” bypassing all controls. Use off so the build fails loudly instead.

Cargo (Rust)
# .cargo/config.toml
[source.crates-io]
replace-with = "escrow"

[source.escrow]
registry = "sparse+http://localhost:7888/cargo/"
Composer (PHP)
{
  "repositories": [
    {"type": "composer", "url": "http://localhost:7888/composer"},
    {"packagist.org": false}
  ]
}
NuGet (.NET)
<!-- nuget.config -->
<configuration>
  <packageSources>
    <clear />
    <add key="escrow" value="http://localhost:7888/nuget/index.json"
         allowInsecureConnections="true" />
  </packageSources>
</configuration>

allowInsecureConnections="true" is required for HTTP sources in NuGet >= 6.8. Use TLS (tls_cert_file / tls_key_file in escrow.toml) to avoid it in production.

Must enable in config: nuget = true under [ecosystems].

Maven
<!-- ~/.m2/settings.xml -->
<settings>
  <mirrors>
    <mirror>
      <id>escrow</id>
      <url>http://localhost:7888/maven2</url>
      <mirrorOf>central</mirrorOf>
      <checksumPolicy>fail</checksumPolicy>
    </mirror>
  </mirrors>
</settings>
Gradle
// settings.gradle.kts
dependencyResolutionManagement {
    repositories { maven(url = "http://localhost:7888/maven2") }
}

Both Maven and Gradle share the same /maven2/ endpoint. Enable with maven = true.


πŸ“Š Dashboard

Real-time package event stream with approve/block controls.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ESCROW  package proxy                         ● LIVE  admin  logoutβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ TIME     β”‚ ECO  β”‚ PACKAGE          β”‚ SIGNAL          β”‚ STATUS     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 14:03:12 β”‚ npm  β”‚ lodash@4.17.21   β”‚ age Β· 2d old    β”‚ BLOCK βœ“   β”‚
β”‚ 14:03:09 β”‚ pypi β”‚ requests@2.31.0  β”‚ osv Β· CVE-...   β”‚ BLOCK βœ“   β”‚
β”‚ 14:03:07 β”‚ go   β”‚ gin@v1.9.1       β”‚ age Β· 1825d old β”‚ ALLOW      β”‚
β”‚ 14:02:48 β”‚ nugetβ”‚ Newtonsoft@13.0  β”‚ age Β· 540d old  β”‚ ALLOW      β”‚
β”‚ 14:02:31 β”‚ mavenβ”‚ spring:core@6.1  β”‚ age Β· 90d old   β”‚ ALLOW      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Access at http://localhost:7888/dashboard. Credentials are printed on first boot.

Approve a blocked package: click βœ“ next to any blocked event. Added to escrow-allowlist.json immediately. No restart needed.

Remove from allowlist: DELETE /dashboard/api/allow with {"ecosystem","name","version"}. All changes are recorded in the live feed with the operator's username.

Block a package manually: POST /dashboard/api/block. Same format.


βš™οΈ Policy Configuration

All policy lives in escrow.toml. Without a [policy] section escrow proxies transparently (with a startup warning).

πŸ—“οΈ Age gate

Blocks packages published fewer than N days ago. Catches injection attacks that publish and spread quickly before the community notices.

[policy.age]
  min_days = 7       # packages must be at least 7 days old
  action   = "block" # block | warn | allow
min_days Use case
1 Catch same-day injections
7 Recommended baseline
30 High-security environments
πŸ” OSV vulnerability scan

Checks every package version against the Open Source Vulnerability database.

[policy.osv]
  min_severity = "MEDIUM"  # LOW | MEDIUM | HIGH | CRITICAL
  action       = "block"

Results are cached 24 hours per version.

πŸ’‘ Fail-open: If the OSV API is unreachable or returns a non-200 response, the vulnerability signal returns skip and the package is allowed through. This is intentional β€” a transient OSV outage should not block all package installs. If you need fail-closed behavior, mirror the OSV database locally or use action = "warn" so blocks require an explicit allowlist entry rather than automatic OSV approval.

πŸ‘€ Publisher account age
[policy.publisher]
  max_account_age_days = 30
  action               = "warn"

For npm: reads _npmUser (set by the registry at publish time). Publisher lookup results are cached 1 hour per account.

πŸ’‘ Fail-open: If the npm registry API is unreachable, the publisher signal returns skip and the package passes through.

πŸ“ˆ Download spike detection
[policy.popularity]
  spike_factor = 10.0  # warn if downloads increased >10Γ— week-over-week
  action       = "warn"
🐍 Block source distributions (PyPI)
[policy.pypi]
  block_sdist = true  # wheel-only; never run setup.py at install time
🚦 Policy actions
action Effect
block Removed from manifest/metadata β€” tools see it as non-existent
warn Allowed through; event logged with WARN status
allow Signal evaluated but never blocks (monitoring mode)

βœ… Allowlist and Blocklist

Via dashboard

Click Approve on any blocked event β€” added to escrow-allowlist.json immediately. Click Block on any allowed event β€” added to escrow-blocklist.json.

Via JSON files

escrow-allowlist.json:

[
  {
    "ecosystem": "npm",
    "name": "lodash",
    "version": "4.17.21",
    "reason": "pinned to known-good version, reviewed by security team",
    "added_by": "admin",
    "added_at": "2026-05-16T14:00:00Z"
  }
]

"version": "" is a wildcard β€” approves all versions of the package.

Allowlist is checked before any policy signal. Approved packages bypass all trust checks and are recorded with signal: override.


🚒 Deployment

πŸ”’ TLS (optional)

Provide a certificate and key to serve HTTPS directly:

[server]
  tls_cert_file = "/etc/ssl/escrow.crt"
  tls_key_file  = "/etc/ssl/escrow.key"

Or terminate TLS at nginx/caddy and set X-Forwarded-Proto: https β€” escrow detects this and sets Secure on session cookies automatically.

⏱️ Write timeout

For large .crate or wheel downloads to slow clients, increase the write timeout:

[server]
  write_timeout_seconds = 300  # default 120
🚧 Rate limiting

Limit requests per IP on proxy endpoints:

[server]
  proxy_rate_limit_per_min = 600  # 0 = disabled (default)
πŸ”— Internal mirrors

Override the upstream URL per ecosystem to point at Nexus, Artifactory, etc.:

[ecosystems]
  npm          = true
  npm_upstream = "https://nexus.corp.internal/repository/npm-proxy/"

  pypi          = true
  pypi_upstream = "https://nexus.corp.internal/repository/pypi-proxy"

  go          = true
  go_upstream = "https://nexus.corp.internal/repository/go-proxy"

  maven                  = true
  maven_upstream         = "https://nexus.corp.internal/repository/maven-releases"
  maven_snapshot_upstream = "https://nexus.corp.internal/repository/maven-snapshots"

Maven SNAPSHOT requests (path contains SNAPSHOT) are routed to maven_snapshot_upstream when set; other requests go to maven_upstream. Without it, all requests share one upstream.

🩺 Health check

GET /healthz probes each enabled upstream with a 3-second HEAD request:

{
  "status": "ok",
  "uptime": "2h34m",
  "storage_backend": "disk",
  "upstream_status": {
    "npm": true, "pypi": true, "go": true,
    "nuget": true, "maven": true
  }
}

Returns HTTP 503 with "status": "degraded" if any upstream is unreachable.

πŸ’Ύ Disk cache

Blobs (tarballs, wheels, JARs) are cached permanently β€” they never expire. Monitor disk usage and plan capacity accordingly:

du -sh ./escrow-cache/blobs/    # how much blob storage is used
find ./escrow-cache/meta/ -name "*.json" | wc -l  # number of metadata entries

There is no built-in eviction. When disk fills, SetBlob fails silently and packages stop being cached (clients still receive them from upstream, but without the cache benefit). The /healthz endpoint returns "cache_writable": false when the cache directory is not writable β€” wire this to your alerting.

For long-running deployments, periodically clean old metadata files:

find ./escrow-cache/meta/ -name "*.json" -mtime +7 -delete

⚠️ Blobs should not be deleted β€” they are the cached packages and their keys are content-addressed.

πŸ–₯️ systemd unit
[Unit]
Description=Escrow package proxy
After=network.target

[Service]
ExecStart=/usr/local/bin/escrow --config=/etc/escrow/escrow.toml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

πŸ—„οΈ Storage

[storage]
  backend = "disk"         # disk | s3 | memory

  [storage.disk]
    path = "./escrow-cache"

  [storage.s3]
    bucket   = "my-escrow-cache"
    region   = "eu-west-1"
    endpoint = ""            # blank = AWS; set for MinIO/Ceph
                             # S3 uploads use temp files, not RAM buffers

What is cached:

Content Backend TTL
npm / PyPI / Composer manifests (filtered) meta 5 min
NuGet registration (filtered) meta 5 min
Maven maven-metadata.xml (filtered) meta 5 min
Go .mod files meta 24h
Go list responses meta 5 min
OSV vulnerability results meta 24h
Publisher account lookups meta 1h
Maven Central version timestamps meta 1h
npm / PyPI / Cargo tarballs, wheels, .crate blob permanent
Go .zip source archives blob permanent
NuGet .nupkg files blob permanent
Maven JARs, POMs, checksums blob permanent

Concurrent cold-cache requests for the same manifest are deduplicated via singleflight β€” only one upstream fetch runs regardless of how many clients ask simultaneously.

Event log persistence β€” add eventlog_path to persist events across restarts:

eventlog_path = "escrow-events.jsonl"  # JSONL append file; empty = in-memory only

Events are loaded from the file on startup (last 500). New events are appended atomically.


πŸ”” Alerts

Send a webhook on every block (Slack, Teams, PagerDuty, custom endpoint):

[alerts]
  webhook_url = "https://hooks.slack.com/services/..."

Payload:

{
  "ecosystem": "npm",
  "package": "malicious@1.0.0",
  "signal": "age",
  "reason": "published 0 day(s) ago (minimum: 7)",
  "action": "block",
  "timestamp": "2026-05-17T14:03:12Z"
}

Webhooks are deduplicated per package per signal during a manifest filter β€” a 200-version package blocked by age sends one webhook, not 200.


πŸ” Security Model

Block by removal, not by error

For npm, PyPI, Composer, NuGet, and Maven, escrow filters blocked versions from the package manifest before returning it. The package manager never learns the version exists β€” it cannot be installed by --force or by a dependency resolver fallback. For Go modules, escrow returns HTTP 403 on .info and @latest endpoints. For Cargo, blocked versions are omitted from the sparse index NDJSON.

Trust pipeline
request β†’ allowlist β†’ blocklist β†’ age β†’ osv β†’ publisher β†’ popularity β†’ allow/warn/block

Allowlist is checked first and short-circuits all other checks β€” an allowlist entry (including wildcard "version": "") bypasses the blocklist and all trust signals. Blocklist is checked second; it can block packages not on the allowlist. Signals run in order; the first block decision terminates the pipeline.

Dashboard security
  • βœ… HMAC-SHA256 session cookies (HttpOnly, SameSite=Strict, 24h TTL)
  • βœ… Timing-safe credential and HMAC comparison (crypto/subtle, hmac.Equal)
  • βœ… Login rate limiting: 10 failures β†’ 15-minute IP lockout
  • βœ… CSRF protection: Origin header checked on all mutating endpoints
  • βœ… Request body limit: 64 KB on all POST/DELETE endpoints
  • βœ… Security headers: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options
No credentials forwarded

Escrow does not store, log, or forward authentication tokens. It acts as an anonymous read-only client to public upstream registries. Private registry authentication (.npmrc tokens, PyPI API keys) is not affected.

Audit trail

Every package evaluation is recorded in the in-memory event log (last 500 events). Dashboard allow/block/remove actions are also recorded with the operator's username. The event stream is available via SSE (/dashboard/api/stream) and REST (/dashboard/api/events).


⚠️ Known Limitations

What escrow does NOT protect against

Postinstall hooks β€” Escrow filters packages from manifests but does not strip postinstall hooks from packages that do pass. You still need ignore-scripts=true (npm/pnpm), enableScripts: false (yarn), or only-binary = [":all:"] (uv) on every developer machine. See the per-tool quickstart guides in docs/quickstart/.

Typosquatting on allowed packages β€” If a package passes the age and vulnerability gates, escrow serves it. Detecting typosquatting requires manual allowlisting or an additional signal not yet implemented.

git dependencies β€” npm git-protocol dependencies (npm install github:user/pkg) bypass the package registry entirely and are not routed through escrow.

Ecosystem limitations

Composer ZIP archives β€” The Composer handler proxies and filters the Packagist v2 metadata (which versions are visible). However, the actual ZIP archive downloads happen via dist.url values in the metadata, which point directly to Packagist's CDN or GitHub. Composer package archives are NOT routed through escrow and are not cached locally. Metadata air-gap is achieved; artifact air-gap is not. If Packagist CDN is unreachable, Composer installs fail.

Unknown publish times β€” When a package's publish time cannot be determined (e.g., Maven Central Search API unavailable, old Packagist entries without timestamps), the age gate treats the package as "ancient" and allows it through. This is fail-open by design to avoid blocking legitimate packages during upstream API outages.

Publisher signal β€” Publisher account age is checked for npm and PyPI only. No equivalent public API exists for Go, Cargo, NuGet, or Maven.

OSV vulnerability scan β€” When the OSV API is unreachable, the signal returns skip and the package passes through (fail-open). See the OSV section above for details.


πŸ”¨ Building from Source

git clone https://github.com/jverhoeks/escrow
cd escrow
go build -o escrow ./cmd/escrow
go test ./...

πŸ“‹ Full Configuration Reference

[server]
  host                     = "127.0.0.1"  # 0.0.0.0 or --host flag for all interfaces
  port                     = 7888
  log_level                = "info"        # debug | info | warn | error
  write_timeout_seconds       = 120  # increase for slow clients downloading large archives
  read_header_timeout_seconds = 10   # time to receive full HTTP request headers (Slowloris defense)
  idle_timeout_seconds        = 120  # keep-alive connection idle limit
  tls_cert_file               = ""   # blank = HTTP only
  tls_key_file                = ""
  proxy_rate_limit_per_min    = 0    # requests/min per IP on proxy endpoints; 0 = disabled

[ecosystems]
  npm      = true
  npm_upstream = ""                  # default https://registry.npmjs.org

  pypi     = true
  pypi_upstream = ""                 # default https://pypi.org

  go       = false
  go_upstream = ""                   # default https://proxy.golang.org

  cargo    = false

  composer = false
  composer_upstream = ""             # default https://repo.packagist.org

  nuget    = false
  nuget_upstream = ""                # default https://api.nuget.org/v3
  nuget_flatcontainer_url = ""       # optional; derived from nuget_upstream for NuGet.org;
                                     # set explicitly for Nexus/Azure Artifacts which use
                                     # different URL schemes (e.g. .../repository/nuget/download)

  maven    = false                   # also covers Gradle
  maven_upstream = ""                # default https://repo1.maven.org/maven2
  maven_snapshot_upstream = ""       # route -SNAPSHOT paths here; default: same as maven_upstream

[storage]
  backend = "disk"         # disk | s3 | memory
  [storage.disk]
    path = "./escrow-cache"
  [storage.s3]
    bucket   = ""
    region   = "eu-west-1"
    endpoint = ""          # blank = AWS S3; set for MinIO

[policy]
  [policy.age]
    min_days = 7
    action   = "block"     # block | warn | allow

  [policy.osv]
    min_severity = "MEDIUM"
    action       = "block"

  [policy.publisher]
    max_account_age_days = 30
    action               = "warn"

  [policy.popularity]
    spike_factor = 10.0
    action       = "warn"

  [policy.pypi]
    block_sdist = false    # true = wheel-only installs

[dashboard]
  enabled  = true
  path     = "/dashboard"
  username = "admin"
  password = ""            # generated on first boot
  secret   = ""            # HMAC session-cookie secret; generated on first boot

[alerts]
  webhook_url = ""

allowlist_path = "escrow-allowlist.json"
blocklist_path = "escrow-blocklist.json"
eventlog_path  = ""        # JSONL file for persistent event log; empty = in-memory only

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL