• v0.9.4 3023889582

    v0.9.4
    All checks were successful
    CI / gate (push) Successful in 1m26s
    CI / image (push) Successful in 54s
    Stable

    joeyr released this 2026-08-10 22:31:01 -05:00 | 84 commits to main since this release

    Hardening and tidying — phase 4 of docs/audit.md, which finishes it. No tool behaviour
    changes; the only runtime difference is a default that production already overrides.

    Changed

    • ALLOWED_HOSTS defaults to the PUBLIC_BASE_URL host plus loopback, instead of
      *. * disables the host check entirely. Production sets the var, so nothing was
      actually open — but the safe default for a service reached through a named proxy is the
      name, and a deployment that forgets the var should not silently lose DNS-rebinding
      protection when PUBLIC_BASE_URL is right there, already required and already
      validated. An explicit value still wins, including an explicit *, which is a
      deliberate local-development choice rather than something to second-guess. Loopback is
      included because the container healthcheck has no proxy in front of it, and the port is
      stripped because Starlette compares against Host without one.

    • line-length = 100 is now a rule. It had been set under [tool.ruff] with nothing
      selecting E501 and the formatter never run, so it read as enforced while nine lines
      exceeded it. E501 is selected and those lines are wrapped. A setting that looks like a
      rule and is not is worse than no setting.

    • deploy/README.md no longer contradicts the release procedure. It said "on a
      release, bump image: there", which was true before the stack moved to :latest and
      survived the change — sending anyone who followed it looking for a line that does not
      exist, and past the re-pull step that actually decides whether a release takes.

    Added

    • X-Frame-Options: DENY and Referrer-Policy: no-referrer on the browser-facing OAuth
      routes.
      /consent is a button that grants a connector access to a personal calendar,
      so it must not be frameable; and the consent URL carries a transaction id whose next hop
      is Forgejo, which does not need it in a Referer.

      Deliberately not a full CSP: FastMCP already sends one for the consent page, and a
      second complete policy is intersected with the first, so a stricter guess made at the
      proxy would break the page it is protecting.

      The rate limit is documented rather than applied, and that is the honest outcome
      rather than a shortfall: limit_req requires a limit_req_zone in nginx's http
      context, and everything in deploy/nginx/calendar.conf is pasted into NPM's Advanced
      tab, which lands inside a server block. Adding limit_req there alone makes
      nginx -t fail and NPM refuse to save the proxy host — breaking the sign-in path the
      change was meant to protect. The two-step fix, including the file to create on the NPM
      host, is recorded in the config next to the block.

    • The resource cache is bounded in principle, not only by its TTL. It is keyed by
      collection and window, so a model sweeping many distinct ranges minted a key each
      time; the 120-second TTL bounded that in practice while nothing bounded it in principle.
      Now capped at 256 entries, evicting expired entries first and then oldest-first, with a
      re-inserted key moved to the back so a hot key is not evicted on its original age.

    Fixed

    • _addressed_event decoded the same event_id up to three times per candidate inside
      its match loop. Decoded once — cosmetic on a handful of occurrences, but the condition
      read as though the three calls could disagree.
    Downloads