Address code review findings across API, web and deployment
release gate / check (push) Failing after 6m43s

High severity:
- wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup
- bucket public rate limits on effective client IP, add per-bookmark budget,
  and forward the signed browser identity from the share page
- disable first-run bootstrap in server deployments and document seeding
- serialize last-admin demotion in one advisory-locked transaction

Medium/low:
- cap and batch share recipient inserts
- add a shared outbound budget for preview/CardDAV/IMAP/signal work
- pin CardDAV dials to 443 and bound IMAP logout
- fix rightward kanban column drags and normalize cards on column delete
- classify backend outages in auth guards, add request timeouts and an
  error boundary, remove the dead login route, surface throttling
- enforce pinned/unpinned page reorder grouping server-side
- avoid preview work for stale bookmark versions; drop duplicate dialog fetch
- sequence-guard collection/tag/calendar/quick-find loads
- validate property config and fix MULTI_SELECT/DATE filter inputs
- document index maintenance; apply same-origin check to sign-out
- trim signup names, unique shared dates and kanban keys (migration 010),
  reset reference cache on identity changes, hide OIDC issuer, return 413
  for oversized direct bodies, escape Markdown link destinations
- remove dead template files and duplicate test helper

Verified with go build/vet, full PostgreSQL race suite, db convergence,
tsc, both ESLint configs and a Next production build.
This commit is contained in:
Andrew S Erwin
2026-10-20 00:13:30 -05:00
parent 5eab58f99c
commit 4e421f6ea2
58 changed files with 1419 additions and 333 deletions
+53 -7
View File
@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
@@ -36,7 +37,19 @@ func read(w http.ResponseWriter, r *http.Request) (object, bool) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var b object
dec := json.NewDecoder(r.Body)
if dec.Decode(&b) != nil || b == nil {
if err := dec.Decode(&b); err != nil {
// Review L-8: an oversized body is 413, not malformed JSON. The Next
// proxy already answers 413; direct API callers deserve the same
// class of error.
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
writeJSON(w, 413, Error{"Request body too large (limit 1 MiB)"})
return nil, false
}
writeJSON(w, 400, Error{"Invalid JSON object"})
return nil, false
}
if b == nil {
writeJSON(w, 400, Error{"Invalid JSON object"})
return nil, false
}
@@ -98,6 +111,27 @@ func ids(b object, k string) ([]string, error) {
}
return out, nil
}
// Share-list caps (review M-1). A 1 MiB JSON body can carry tens of
// thousands of ids; each one used to become an INSERT in a user-locked
// transaction. These are product limits, not storage limits.
const (
maxShareRecipients = 100
maxSharedDateRecipients = 200
)
// cappedIDs is ids plus a length cap (review M-1), returning a clear 400.
func cappedIDs(b object, key string, max int) ([]string, error) {
list, e := ids(b, key)
if e != nil {
return nil, e
}
if len(list) > max {
return nil, bad(fmt.Sprintf("%s may contain at most %d entries", key, max))
}
return list, nil
}
func fields(b object, allowed string) object {
out := object{}
for _, k := range strings.Fields(allowed) {
@@ -254,11 +288,23 @@ func reorder(r *http.Request, table, col string, order []string) error {
return nil
})
}
func shareRows(r *http.Request, tx *sql.Tx, table, key, id string, list []string) error {
for _, u := range list {
if _, e := tx.ExecContext(r.Context(), `INSERT INTO `+db.Quote(table)+` (`+db.Quote(key)+`,"userId") VALUES($1,$2) ON CONFLICT DO NOTHING`, id, u); e != nil {
return e
}
// outboundBusy answers 429 with Retry-After when the shared outbound budget
// is exhausted (review M-2).
func outboundBusy(w http.ResponseWriter, retry time.Duration) {
if retry < time.Second {
retry = time.Second
}
return nil
w.Header().Set("Retry-After", strconv.Itoa(int(retry.Seconds())+1))
writeJSON(w, 429, Error{"Too much outbound work right now; try again shortly"})
}
// shareRows inserts every recipient in ONE statement (review M-1). table and
// key come only from the server-side call sites, never from request input.
func shareRows(r *http.Request, tx *sql.Tx, table, key, id string, list []string) error {
if len(list) == 0 {
return nil
}
_, e := tx.ExecContext(r.Context(), `INSERT INTO `+db.Quote(table)+` (`+db.Quote(key)+`,"userId") SELECT $1,unnest($2::text[]) ON CONFLICT DO NOTHING`, id, list)
return e
}