Skip to content

feat: resilient integrations, column caching, SSRF fix + tests, and one-click Docker/Railway hosting#100

Merged
aaronjmars merged 5 commits into
mainfrom
feat/hosting-and-hardening
Jul 9, 2026
Merged

feat: resilient integrations, column caching, SSRF fix + tests, and one-click Docker/Railway hosting#100
aaronjmars merged 5 commits into
mainfrom
feat/hosting-and-hardening

Conversation

@aaronjmars

Copy link
Copy Markdown
Collaborator

What & why

Five focused changes: harden the integrations layer, cut redundant upstream traffic, close a webhook SSRF hole (with a new test suite), and make Minitor one-click self-hostable on Docker / Railway.

Commits

  1. feat(integrations) — new lib/integrations/fetch.ts (fetchUpstream): per-attempt AbortSignal.timeout, jittered exponential backoff on retryable statuses (429/408/5xx) + network errors, Retry-After parsing, and a typed UpstreamError with friendly rate-limit messaging. All 25 upstream integrations routed through it, so a slow or rate-limiting provider no longer hangs a column or leaks a raw fetch error.
  2. perf(columns)cachedColumnFetch, keyed on typeId + config + cursor. Identical concurrent requests (duplicate columns, "Refresh all") share one in-flight promise; results are reused for a few seconds so bursts don't fan out to the same upstream.
  3. fix(security)validateWebhookUrl trusted URL.hostname, which keeps IPv6 brackets ([::1]), so the private-range guard never matched and loopback/link-local IPv6 targets were allowed. Now strips brackets, handles IPv4-mapped IPv6 (dotted + hex-compressed), and broadens link-local matching. Adds a vitest suite (271 tests): plugin contract, SSRF guard, colour normalisation, keyword matching, fetch cache, fetchUpstream, and a deck export→import round-trip (against a true in-memory PGlite — so memory:// in DATABASE_URL now means ephemeral in-memory rather than silently writing to on-disk .minitor/pgdata).
  4. feat(hosting) — multi-stage Dockerfile (standalone output), docker-compose.yml (app + Postgres, migrate-on-boot), docker-entrypoint.sh, railway.json, .dockerignore. Optional single-password gate via middleware.ts (HTTP Basic Auth, off when MINITOR_PASSWORD unset) with a public /api/health probe. Hosted mode (lib/hosted.ts): setEnvKeys refuses to run and the settings key editor is read-only, since keys come from the environment on a shared public instance.
  5. chore(deps) — add vitest (+ test/test:watch scripts) and regenerate package-lock.json so nested esbuild@0.28.1 + platform binaries are fully recorded (npm 11 tolerated the omission; npm ci in a clean container did not).

Verification

  • tsc --noEmit clean · 271/271 vitest tests pass
  • ✅ Docker image builds; docker compose up runs migrations on boot and serves gated
  • Deployed live to Railway (Dockerfile builder + Postgres plugin): healthcheck 200, gate returns 401/401/200 for no-auth/wrong-pass/correct-pass, all 10 migrations applied against Postgres, and a live hacker-news fetch returned real items through fetchUpstream + cache

Notes

  • No behaviour change for local ./minitor: the password gate and hosted mode are both off unless their env vars are set.
  • Hosted instances configure API keys via environment variables (the in-app editor is intentionally disabled in hosted mode).

…ndling

Introduce lib/integrations/fetch.ts — a drop-in fetch() replacement that adds
a per-attempt AbortSignal.timeout, jittered exponential backoff on retryable
statuses (429/408/5xx) and network errors, Retry-After parsing, and a typed
UpstreamError with friendly rate-limit messaging. Route all 25 upstream
integrations through it so a slow or rate-limiting provider no longer hangs a
column or surfaces a raw fetch failure.
Wrap the column fetcher in cachedColumnFetch, keyed on typeId + config +
cursor. Identical concurrent requests (duplicate columns, Refresh all) share a
single in-flight promise, and results are reused for a few seconds so a burst
of refreshes no longer fans out to the same upstream repeatedly.
…uite

validateWebhookUrl trusted URL.hostname, which keeps IPv6 brackets ([::1]), so
the private-range guard never matched and loopback/link-local IPv6 targets were
allowed. Strip brackets before checking, handle IPv4-mapped IPv6 (dotted and
hex-compressed), and broaden the link-local match.

Add a vitest suite (271 tests): plugin contract, SSRF guard, column colour
normalisation, keyword matching, fetch cache, fetchUpstream, and a deck
export->import round-trip. The round-trip runs against a true in-memory PGlite,
so make memory:// in DATABASE_URL mean an ephemeral in-memory database instead
of silently writing to on-disk .minitor/pgdata (the migrate script skips it).
@gitguardian

gitguardian Bot commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
34690012 Triggered Generic Password 2b7fdea docker-compose.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@aaronjmars aaronjmars force-pushed the feat/hosting-and-hardening branch 2 times, most recently from dfe986c to 13c7d47 Compare July 9, 2026 18:44
Add a multi-stage Dockerfile (Next.js standalone output), docker-compose.yml
(app + Postgres, migrate-on-boot), docker-entrypoint.sh, railway.json, and a
.dockerignore for one-command self-hosting.

Introduce an optional single-password gate via middleware.ts (HTTP Basic Auth,
off when MINITOR_PASSWORD is unset) with a public /api/health probe, plus a
hosted mode (lib/hosted.ts): setEnvKeys refuses to run and the settings key
editor renders read-only, since keys come from the environment on a shared
public instance. Set output: standalone in next.config.ts and document the
deploy paths in the README.
…naries

Add vitest (+ test/test:watch scripts). Regenerate package-lock.json so the
nested esbuild@0.28.1 and its platform binaries are fully recorded — npm 11
tolerated the omission locally but npm ci in a clean container failed on the
missing optional deps.
@aaronjmars aaronjmars force-pushed the feat/hosting-and-hardening branch from 13c7d47 to 62dd499 Compare July 9, 2026 18:50
@aaronjmars aaronjmars merged commit 21dcf2d into main Jul 9, 2026
1 of 2 checks passed
@aaronjmars aaronjmars deleted the feat/hosting-and-hardening branch July 9, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant