Commit Graph
3 Commits
Author SHA1 Message Date
ClaudeandClaude Opus 5 ba107c1b53 docs: this describes the prod deployment now, not beta
granthi-link has run against the PROD forge since the promotion, but every
document in this repo still described the beta tier. Someone following the
README would expect their device to land on granthi-beta.shre.ai; it lands on
granthi.shre.ai. Two of the statements were not merely stale but false:
"tailnet-only" and "Not publicly exposed" — the service has been public at
https://granthi-link.shre.ai since 2026-08-23, and that 404 at / (no root
route) has twice been misread as an outage.

Verified on [email protected], 2026-08-30:
  gitea_base         = http://127.0.0.1:3040   (gitea-central-gitea-1)
  public_gitea_base  = https://granthi.shre.ai
  systemctl is-active granthi-link -> active
  https://granthi-link.shre.ai/health -> 200 {"status":"ok","version":"1.1.0"}
  rate_limit: trust_forwarded_for true, trusted_proxies ["100.107.37.98/32"],
              no "rules" key -> falls back to DEFAULT_RATE_RULES

Docs only. No server, client or test code is touched, and the deployed
service is NOT redeployed by this change — it still reports 1.1.0 against a
v1.2 repo, which is recorded separately.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01L1b6BN9TZVxHkmignRq4p8
2026-08-29 23:20:30 -04:00
Nirav PatelandClaude Opus 5 e2fed5886f feat(link): rate-limit /v1/link and /v1/repos
Last unbuilt item on the promotion-window hardening checklist. /v1/link
round-trips Zitadel, can CREATE a forge account and always mints a token, so
it is the endpoint that must not be free to hammer.

Sliding window per (route, client), ON by default -- unlimited has to be a
deliberate config act, not an omission. Defaults 5/hour and 60/hour; /health
never limited. 429 + Retry-After, decided BEFORE the body is read so an
abusive caller costs nothing.

Decisions worth naming:
- State is an in-process dict behind a lock. granthi-link is ONE
  ThreadingHTTPServer, so that IS the store -- no redis. Kept behind a class
  so a future multi-process move has one thing to change.
- Denied requests are NOT recorded. Recording them lets a hammering client
  push its own window forward and lock itself out forever.
- Key store is capped; at capacity it drops least-recent windows and logs
  loudly. Fail-open under key pressure, chosen over an unbounded dict that
  is a memory DoS.
- trust_forwarded_for OFF by default. Behind cloudflared every request comes
  from the tunnel, so limiting on the socket peer starves everyone; but XFF
  is client-controlled. A caller can PREPEND, a trusted proxy APPENDS what it
  actually saw -- so we read the LAST entry, never the first.
- A malformed rule refuses startup instead of silently meaning unlimited.

Tests 69 -> 87, including a 40-thread race proving the lock holds, the
self-lockout case, XFF spoof-resistance, and a real 429 on the wire.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LTARYHX7GPepi3CH3tp5pg
2026-08-22 23:14:54 -04:00
Nirav PatelandClaude Fable 5 2077591755 granthi-sync v1: granthi-link provisioning service + client daemon + tests
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 00:09:19 -04:00