Skip to content

TW-4877: webhook server preflight UX + security hardening#63

Open
qasim-nylas wants to merge 1 commit intomainfrom
feature/TW-4877-webhook-preflight-hardening
Open

TW-4877: webhook server preflight UX + security hardening#63
qasim-nylas wants to merge 1 commit intomainfrom
feature/TW-4877-webhook-preflight-hardening

Conversation

@qasim-nylas
Copy link
Copy Markdown
Collaborator

@qasim-nylas qasim-nylas commented Apr 27, 2026

Summary

  • Interactive preflight on nylas webhook server — detects cloudflared, offers brew install on macOS, prompts to enable tunnel, reads secret with echo disabled.
  • New --no-tunnel flag; loopback-only output no longer instructs user to register localhost with Nylas.
  • Webhookserver hardening: 1 MiB body cap, replay-protection (MaxEventAge), bounded handler goroutines, events_dropped in /health.
  • Crypto: Argon2id t=1t=3, 12-char passphrase floor, derived keys zeroed.
  • XSS fix in Air AI summary modal; scheme allow-list on notetaker-open-external.
  • Silent-failure cleanup in pattern_learner, doctor_checks, base_client.go, requestLocation, admin.go pagination.

Origin: Slack #cli thread — Nick reported the localhost-URL UX issue.

Jira: TW-4877

Test plan

  • make ci green (build, vet, lint, race, security, govulncheck)
  • nylas webhook server (TTY, no flags) prompts for tunnel; Ctrl-D exits cleanly
  • nylas webhook server --no-tunnel runs loopback-only without prompt
  • Webhook receiver returns 413 on >1 MiB bodies
  • Air UI loads; AI summary modal renders without XSS

Originating issue: nylas webhook server displayed a localhost URL and told
the user to register it with Nylas — but Nylas can't reach localhost, so
events never fired (Slack #cli, 2026-04-27).

Webhook preflight (the originating issue)
- Interactive preflight when neither --tunnel nor --no-tunnel is set:
  detect cloudflared, offer brew install on macOS, prompt to enable
  tunnel, read secret with terminal echo disabled.
- New --no-tunnel flag for non-interactive opt-out.
- Loopback-only output no longer instructs user to register localhost.
- Prompter interface so EOF (Ctrl-D) propagates cleanly instead of
  silently flipping into --allow-unsigned or auto-running brew install.

Webhook server hardening (internal/adapters/webhookserver)
- 1 MiB request body cap (http.MaxBytesReader).
- Replay protection via MaxEventAge (CloudEvents `time` skew check).
- Goroutine fanout bounded by 32-slot semaphore + per-handler recover().
- events_dropped surfaced in /health.
- Event-display goroutine exits on ctx.Done() and recovers from panics.

Other silent-failure fixes uncovered in review
- pattern_learner: surface per-calendar errors instead of silent skip.
- doctor_checks: propagate config/secret-store failures explicitly.
- base_client.go: error on malformed tool-call JSON.
- requestLocation: return error on bad timezone instead of UTC fallback.
- admin.go: cycle guard + maxGrantPages ceiling on cursor pagination.

Crypto hardening (internal/adapters/keyring)
- Argon2id raised from t=1 to t=3.
- NYLAS_FILE_STORE_PASSPHRASE minimum length 12.
- Derived AES keys zeroed after use.

Frontend (Air)
- XSS fix: AI summary sentiment/category now escapeHtml-ed.
- notetaker-open-external: scheme allow-list (https?://) + noopener.

Tests
- New unit tests for preflightTunnelChoice (mocked Prompter).
- New webhookserver tests: oversized body 413, replay window, loopback
  bind, events_dropped in /health.
- webguard middleware: Referer-only fallback path.
- All packages pass go test -race; golangci-lint clean.
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