Pass the authenticated viewer to Changelog.timeline/2 and projection/2 for #70

c3164ba0e293 · AtlantisPleb · · parent 5b7915942687

Pass the authenticated viewer to Changelog.timeline/2 and projection/2 for #70

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/changelog.ex
  • modified lib/openagents_web/controllers/changelog_controller.ex
  • modified lib/openagents_web/live/changelog_live.ex
  • added test/openagents_web/controllers/changelog_controller_test.exs

Diff

4 files changed, +95 -14

lib/openagents/changelog.ex modified +20 -11

@@ -36,10 +36,15 @@ defmodule OpenAgents.Changelog do

36 36
  ledger. Pass `refresh: true` to bypass the cache (PubSub-driven callers).
37 37
  """
38 38
  def timeline(repo, opts \\ []) do
39
    viewer = opts[:viewer]
40
39 41
    cond do
40 42
      not Visibility.allows?(repo, :ledger) -> {:error, :not_public}
41
      opts[:refresh] -> {:ok, put_cache(repo, build(repo))}
42
      true -> {:ok, cached(repo)}
43
      opts[:refresh] ->
44
        rows = build(repo, viewer)
45
        if is_nil(viewer), do: put_cache(repo, rows)
46
        {:ok, rows}
47
      true -> {:ok, cached(repo, viewer)}
43 48
    end
44 49
  end
45 50

@@ -69,12 +74,16 @@ defmodule OpenAgents.Changelog do

69 74
70 75
  # ── assembly ─────────────────────────────────────────────────────────────
71 76
72
  defp cached(repo) do
73
    now = System.monotonic_time(:millisecond)
77
  defp cached(repo, viewer) do
78
    if is_nil(viewer) do
79
      now = System.monotonic_time(:millisecond)
74 80
75
    case :persistent_term.get(@cache_key, nil) do
76
      {^repo, at, rows} when now - at < @cache_ttl_ms -> rows
77
      _ -> put_cache(repo, build(repo))
81
      case :persistent_term.get(@cache_key, nil) do
82
        {^repo, at, rows} when now - at < @cache_ttl_ms -> rows
83
        _ -> put_cache(repo, build(repo, nil))
84
      end
85
    else
86
      build(repo, viewer)
78 87
    end
79 88
  end
80 89

@@ -83,7 +92,7 @@ defmodule OpenAgents.Changelog do

83 92
    rows
84 93
  end
85 94
86
  defp build(repo) do
95
  defp build(repo, viewer) do
87 96
    entries = safely(fn -> authored_entries(repo) end, [])
88 97
    receipts = safely(fn -> receipt_index(repo) end, %{pushes: [], builds: [], deploys: []})
89 98

@@ -98,10 +107,10 @@ defmodule OpenAgents.Changelog do

98 107
    (authored ++ uncovered)
99 108
    |> Enum.sort_by(& &1.entry_at, {:desc, DateTime})
100 109
    |> Enum.take(@entry_limit)
101
    |> redact_rows()
110
    |> redact_rows(viewer)
102 111
  end
103 112
104
  defp redact_rows(rows) do
113
  defp redact_rows(rows, viewer) do
105 114
    link_ids =
106 115
      rows
107 116
      |> Enum.map(& &1[:artifact_link_id])

@@ -120,7 +129,7 @@ defmodule OpenAgents.Changelog do

120 129
121 130
    Enum.map(rows, fn row ->
122 131
      link = row[:artifact_link_id] && Map.get(by_id, row[:artifact_link_id])
123
      Transparency.redact_for_viewer(row, link)
132
      Transparency.redact_for_viewer(row, link, viewer)
124 133
    end)
125 134
  end
126 135
lib/openagents_web/controllers/changelog_controller.ex modified +2 -1

@@ -11,8 +11,9 @@ defmodule OpenAgentsWeb.ChangelogController do

11 11
12 12
  def show(conn, params) do
13 13
    repo = Map.get(params, "repo", "openagents.com")
14
    viewer = conn.assigns[:current_user]
14 15
15
    case OpenAgents.Changelog.projection(repo) do
16
    case OpenAgents.Changelog.projection(repo, viewer: viewer) do
16 17
      {:ok, payload} ->
17 18
        json(conn, payload)
18 19
lib/openagents_web/live/changelog_live.ex modified +2 -2

@@ -20,7 +20,7 @@ defmodule OpenAgentsWeb.ChangelogLive do

20 20
  @impl true
21 21
  def mount(_params, _session, socket) do
22 22
    rows =
23
      case Changelog.timeline(@repo) do
23
      case Changelog.timeline(@repo, viewer: socket.assigns.current_user) do
24 24
        {:ok, rows} -> rows
25 25
        {:error, :not_public} -> raise OpenAgentsWeb.PublicNotFoundError
26 26
      end

@@ -53,7 +53,7 @@ defmodule OpenAgentsWeb.ChangelogLive do

53 53
             :forge_build_ready,
54 54
             :forge_deploy
55 55
           ] do
56
    case Changelog.timeline(@repo, refresh: true) do
56
    case Changelog.timeline(@repo, refresh: true, viewer: socket.assigns.current_user) do
57 57
      {:ok, rows} -> {:noreply, assign(socket, :rows, rows)}
58 58
      _ -> {:noreply, socket}
59 59
    end
test/openagents_web/controllers/changelog_controller_test.exs added +71

@@ -0,0 +1,71 @@

1
defmodule OpenAgentsWeb.ChangelogControllerTest do
2
  @moduledoc """
3
  The `/api/changelog` controller (#138, TRANSPARENCY-001): the public,
4
  schema-versioned projection is redacted per the requester's viewer and the
5
  entry's transparency tier.
6
  """
7
8
  use OpenAgentsWeb.ConnCase, async: false
9
10
  alias OpenAgents.Changelog
11
12
  setup do
13
    :persistent_term.erase({OpenAgents.Changelog, :cache})
14
    on_exit(fn -> :persistent_term.erase({OpenAgents.Changelog, :cache}) end)
15
    :ok
16
  end
17
18
  defp full_sha(prefix), do: String.pad_trailing(prefix, 40, "0")
19
20
  describe "GET /api/changelog" do
21
    test "redacts dark-tier trace_ref, trace_digest, and detail", %{conn: conn} do
22
      {:ok, _} =
23
        Changelog.record(%{
24
          repo: "openagents.com",
25
          sha: full_sha("feed0201"),
26
          summary: "Dark tier entry",
27
          category: "ui",
28
          source: "operator",
29
          entry_at: DateTime.utc_now(),
30
          visibility: "l2",
31
          transparency_tier: "dark",
32
          trace_ref: "trace:v1:dark",
33
          trace_digest: "sha256:dark",
34
          detail: %{"note" => "hidden"}
35
        })
36
37
      conn = get(conn, ~p"/api/changelog")
38
      payload = json_response(conn, 200)
39
40
      assert [entry] = Enum.filter(payload["entries"], &(&1["summary"] == "Dark tier entry"))
41
      assert entry["trace_ref"] == nil
42
      assert entry["trace_digest"] == nil
43
      assert entry["detail"] == %{}
44
    end
45
46
    test "keeps pulse-tier trace_ref and trace_digest but redacts detail", %{conn: conn} do
47
      {:ok, _} =
48
        Changelog.record(%{
49
          repo: "openagents.com",
50
          sha: full_sha("feed0202"),
51
          summary: "Pulse tier entry",
52
          category: "ui",
53
          source: "operator",
54
          entry_at: DateTime.utc_now(),
55
          visibility: "l2",
56
          transparency_tier: "pulse",
57
          trace_ref: "trace:v1:pulse",
58
          trace_digest: "sha256:pulse",
59
          detail: %{"note" => "hidden"}
60
        })
61
62
      conn = get(conn, ~p"/api/changelog")
63
      payload = json_response(conn, 200)
64
65
      assert [entry] = Enum.filter(payload["entries"], &(&1["summary"] == "Pulse tier entry"))
66
      assert entry["trace_ref"] == "trace:v1:pulse"
67
      assert entry["trace_digest"] == "sha256:pulse"
68
      assert entry["detail"] == %{}
69
    end
70
  end
71
end

This page updates live while a promote is in flight · changelog