Tablet 46: the watch — rate limits at every gate, per IP at the pe'ah, per key inside
Tamid §1.1 wrote the pe'ah's limit (10/min, 100/h per IP); nothing enforced it, on any tier, and every admitted request spends manna. VavPipeline.BigdeiKodesh.Mishmar: fixed minute and hour windows per subject in ETS, the subject stored only as sha256(boot salt, tier, subject) — no IP and no key is ever in memory as itself, the sweep drops closed windows, nothing survives a restart (the Tamid's "not logged beyond the current request", kept in memory too). A refused knock is not counted. 429 RATE_LIMITED with Retry-After and the manna explanation (Shemot 16:18). Pe'ah per peer address; courtyard and chatzer per key, after the gate (a wrong key is 401 and never counted). Numbers: pe'ah 10/100 (Tamid), courtyard 20/200 and chatzer 5/60 (from Construction §2.3; config :rate_limits). Service shorashim-external: externalTrafficPolicy Local. With Cluster the pod saw every caller as the node, 91.92.153.113 (observed via /proc/net/tcp) — a per-IP limit would have been one bucket for the world. Spec: tamid §1.1 (watch, 429 rows, the Service note), KOHEN.md. Doorpost 42b38626… → e4e3d56d…. tablet46_mishmar_test; Elixir 533/0 (L3 on and off). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
79eb34840f
commit
54ca651f37
12 files changed
+406
-17
No files matched your search
@@ -28,7 +28,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
|
||||
require Logger
|
||||
|
||||
alias VavPipeline.BigdeiKodesh.Gate
|
||||
alias VavPipeline.BigdeiKodesh.{Gate, Mishmar}
|
||||
|
||||
# Form 3 architectural enforcement (vehaya §3.2). NoTrackingLogger
|
||||
# replaces Plug.Logger; sanitized paths on /peah/* honor the
|
||||
@@ -63,17 +63,21 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
end
|
||||
|
||||
post "/peah/ask" do
|
||||
case conn.body_params do
|
||||
%{"question" => question} when is_binary(question) and question != "" ->
|
||||
process_peah(conn, question)
|
||||
# Tamid §1.1: 10 a minute, 100 an hour, per IP; the IP is used for
|
||||
# this and nothing else (Mishmar keeps a salted hash, not the IP).
|
||||
under_the_watch(conn, :peah, Mishmar.peer(conn), fn conn ->
|
||||
case conn.body_params do
|
||||
%{"question" => question} when is_binary(question) and question != "" ->
|
||||
process_peah(conn, question)
|
||||
|
||||
_ ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
|
||||
issue: "Missing or empty 'question' field",
|
||||
correction: "Submit JSON with a 'question' string",
|
||||
resubmit: true
|
||||
})
|
||||
end
|
||||
_ ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
|
||||
issue: "Missing or empty 'question' field",
|
||||
correction: "Submit JSON with a 'question' string",
|
||||
resubmit: true
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
@@ -129,6 +133,32 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
})
|
||||
end
|
||||
|
||||
# The watch (Mishmar): how often one subject may knock at a tier.
|
||||
# 429 carries the manna explanation Tamid §1.1 asks for, and Retry-After.
|
||||
defp under_the_watch(conn, tier, subject, serve) do
|
||||
case Mishmar.knock(tier, subject) do
|
||||
{:ok, _remaining} ->
|
||||
serve.(conn)
|
||||
|
||||
{:error, window, retry_after} ->
|
||||
limits = Mishmar.limits(tier)
|
||||
|
||||
conn
|
||||
|> put_resp_header("retry-after", Integer.to_string(retry_after))
|
||||
|> send_error(429, "RATE_LIMITED", %{
|
||||
issue:
|
||||
"Too many requests this #{window}: the #{tier} allows " <>
|
||||
"#{limits[:minute]} a minute and #{limits[:hour]} an hour per caller",
|
||||
correction: "Wait #{retry_after}s and resubmit",
|
||||
manna:
|
||||
"The day's provision is one portion for everyone who comes (Shemot 16:18 — " <>
|
||||
"the one who gathered much had nothing over). The watch keeps one caller " <>
|
||||
"from gathering the whole omer.",
|
||||
resubmit: true
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# === COURTYARD — Authenticated ===
|
||||
#
|
||||
# The key is checked against the keys file (VavPipeline.BigdeiKodesh.Gate),
|
||||
@@ -136,7 +166,10 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
defp through_the_gate(conn, tier, serve) do
|
||||
case Gate.admit(conn, tier) do
|
||||
{:ok, _granted} ->
|
||||
serve.(conn)
|
||||
# Past the gate, the key is the hand that knocks: the watch
|
||||
# counts per key, not per IP. A refused key never reaches it.
|
||||
{:ok, key} = Gate.bearer(conn)
|
||||
under_the_watch(conn, tier, key, serve)
|
||||
|
||||
{:error, :tier_too_low} ->
|
||||
send_error(conn, 403, "FORBIDDEN", %{
|
||||
@@ -951,6 +984,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
defp error_type("HALT" <> _), do: "cardinal_sin_detected"
|
||||
defp error_type("UNAUTHORIZED"), do: "authentication_required"
|
||||
defp error_type("FORBIDDEN"), do: "insufficient_tier"
|
||||
defp error_type("RATE_LIMITED"), do: "too_many_requests"
|
||||
defp error_type("TEIKU" <> _), do: "unresolvable"
|
||||
defp error_type("LO_TOV"), do: "specification_mismatch"
|
||||
defp error_type("CHATAT" <> _), do: "correction_needed"
|
||||
|
||||
Reference in new issue
Block a user