dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

add getCurrentUserVouches and getRemoteVouches

Luna 35d9d5fe 8a36f6a3

+399
+9
appview/lib/atvouch/lexicons/defs.ex
··· 1 + defmodule Atvouch.Lexicons.Graph.Defs do 2 + use Atex.Lexicon 3 + 4 + deflexicon( 5 + Jason.decode!( 6 + File.read!(Path.join([File.cwd!(), "..", "lexicons", "dev", "atvouch", "graph", "defs.json"])) 7 + ) 8 + ) 9 + end
+11
appview/lib/atvouch/lexicons/get_current_user_vouches.ex
··· 1 + defmodule Atvouch.Lexicons.Graph.GetCurrentUserVouches do 2 + use Atex.Lexicon 3 + 4 + deflexicon( 5 + Jason.decode!( 6 + File.read!( 7 + Path.join([File.cwd!(), "..", "lexicons", "dev", "atvouch", "graph", "getCurrentUserVouches.json"]) 8 + ) 9 + ) 10 + ) 11 + end
+11
appview/lib/atvouch/lexicons/get_remote_vouches.ex
··· 1 + defmodule Atvouch.Lexicons.Graph.GetRemoteVouches do 2 + use Atex.Lexicon 3 + 4 + deflexicon( 5 + Jason.decode!( 6 + File.read!( 7 + Path.join([File.cwd!(), "..", "lexicons", "dev", "atvouch", "graph", "getRemoteVouches.json"]) 8 + ) 9 + ) 10 + ) 11 + end
+56
appview/lib/atvouch/xrpc_router.ex
··· 11 11 |> send_resp(200, Jason.encode!(%{alive: true})) 12 12 end 13 13 14 + alias Atvouch.Lexicons.Graph.Defs 15 + alias Atvouch.Lexicons.Graph.GetCurrentUserVouches 16 + alias Atvouch.Lexicons.Graph.GetRemoteVouches 17 + 18 + get "/dev.atvouch.graph.getCurrentUserVouches" do 19 + with {:ok, did} <- require_auth(conn) do 20 + vouches = 21 + did 22 + |> Atvouch.Vouch.all_from_did() 23 + |> Enum.map(&to_vouch_view/1) 24 + 25 + output = %GetCurrentUserVouches.Output{vouches: vouches} 26 + 27 + conn 28 + |> put_resp_content_type("application/json") 29 + |> send_resp(200, Jason.encode!(output)) 30 + end 31 + end 32 + 33 + get "/dev.atvouch.graph.getRemoteVouches" do 34 + with {:ok, did} <- require_auth(conn) do 35 + vouches = 36 + did 37 + |> Atvouch.Vouch.all_targeting_did() 38 + |> Enum.map(&to_vouch_view/1) 39 + 40 + output = %GetRemoteVouches.Output{vouches: vouches} 41 + 42 + conn 43 + |> put_resp_content_type("application/json") 44 + |> send_resp(200, Jason.encode!(output)) 45 + end 46 + end 47 + 14 48 match _ do 15 49 conn 16 50 |> put_resp_content_type("application/json") 17 51 |> send_resp(404, Jason.encode!(%{error: "MethodNotImplemented", message: "XRPC method not found"})) 52 + end 53 + 54 + defp require_auth(conn) do 55 + case conn.assigns[:user_did] do 56 + nil -> 57 + conn 58 + |> put_resp_content_type("application/json") 59 + |> send_resp(401, Jason.encode!(%{error: "AuthRequired", message: "Authentication required"})) 60 + |> halt() 61 + 62 + did -> 63 + {:ok, did} 64 + end 65 + end 66 + 67 + defp to_vouch_view(vouch) do 68 + %Defs.VouchView{ 69 + uri: vouch.at_uri, 70 + creatorDid: vouch.creator_did, 71 + targetDid: vouch.target_did, 72 + createdAt: vouch.original_created_at 73 + } 18 74 end 19 75 end
+184
appview/test/atvouch/xrpc_vouches_test.exs
··· 1 + defmodule Atvouch.XrpcVouchesTest do 2 + use ExUnit.Case 3 + import Plug.Test 4 + import Plug.Conn 5 + 6 + @opts Atvouch.Router.init([]) 7 + 8 + setup do 9 + Ecto.Adapters.SQL.Sandbox.checkout(Atvouch.Repo) 10 + Ecto.Adapters.SQL.Sandbox.mode(Atvouch.Repo, {:shared, self()}) 11 + 12 + {_server_pid, auth_port} = Atvouch.Test.FakeAuthServer.start() 13 + auth_url = "http://127.0.0.1:#{auth_port}" 14 + prev_auth_url = Application.get_env(:atvouch, :auth_url) 15 + Application.put_env(:atvouch, :auth_url, auth_url) 16 + 17 + on_exit(fn -> 18 + Application.put_env(:atvouch, :auth_url, prev_auth_url) 19 + end) 20 + 21 + # Create test identities and vouches 22 + {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:alice", handle: "alice.test"}) 23 + {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:bob", handle: "bob.test"}) 24 + {:ok, _} = Atvouch.Identity.create(%{did: "did:plc:carol", handle: "carol.test"}) 25 + 26 + # Alice vouched for Bob 27 + {:ok, _} = 28 + Atvouch.Vouch.create(%{ 29 + at_uri: "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:bob", 30 + creator_did: "did:plc:alice", 31 + target_did: "did:plc:bob", 32 + original_created_at: "2026-03-01T00:00:00Z", 33 + remote_created_at: "2026-03-01T00:00:00Z", 34 + at_cid: "bafyabc123", 35 + live: true 36 + }) 37 + 38 + # Alice vouched for Carol 39 + {:ok, _} = 40 + Atvouch.Vouch.create(%{ 41 + at_uri: "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:carol", 42 + creator_did: "did:plc:alice", 43 + target_did: "did:plc:carol", 44 + original_created_at: "2026-03-02T00:00:00Z", 45 + remote_created_at: "2026-03-02T00:00:00Z", 46 + at_cid: "bafyabc456", 47 + live: true 48 + }) 49 + 50 + # Bob vouched for Alice 51 + {:ok, _} = 52 + Atvouch.Vouch.create(%{ 53 + at_uri: "at://did:plc:bob/dev.atvouch.graph.vouch/did:plc:alice", 54 + creator_did: "did:plc:bob", 55 + target_did: "did:plc:alice", 56 + original_created_at: "2026-03-03T00:00:00Z", 57 + remote_created_at: "2026-03-03T00:00:00Z", 58 + at_cid: "bafyabc789", 59 + live: true 60 + }) 61 + 62 + :ok 63 + end 64 + 65 + describe "GET /xrpc/dev.atvouch.graph.getCurrentUserVouches" do 66 + test "returns vouches created by the authenticated user" do 67 + conn = 68 + conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches") 69 + |> put_req_header("authorization", "Bearer valid-token:did:plc:alice") 70 + |> Atvouch.Router.call(@opts) 71 + 72 + assert conn.status == 200 73 + body = Jason.decode!(conn.resp_body) 74 + assert is_list(body["vouches"]) 75 + assert length(body["vouches"]) == 2 76 + 77 + uris = Enum.map(body["vouches"], & &1["uri"]) 78 + assert "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:bob" in uris 79 + assert "at://did:plc:alice/dev.atvouch.graph.vouch/did:plc:carol" in uris 80 + 81 + vouch = Enum.find(body["vouches"], &(&1["uri"] =~ "did:plc:bob")) 82 + assert vouch["creatorDid"] == "did:plc:alice" 83 + assert vouch["targetDid"] == "did:plc:bob" 84 + assert vouch["createdAt"] == "2026-03-01T00:00:00Z" 85 + end 86 + 87 + test "returns empty list when user has no vouches" do 88 + conn = 89 + conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches") 90 + |> put_req_header("authorization", "Bearer valid-token:did:plc:carol") 91 + |> Atvouch.Router.call(@opts) 92 + 93 + assert conn.status == 200 94 + body = Jason.decode!(conn.resp_body) 95 + assert body["vouches"] == [] 96 + end 97 + 98 + test "returns 401 without authentication" do 99 + conn = 100 + conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches") 101 + |> Atvouch.Router.call(@opts) 102 + 103 + assert conn.status == 401 104 + end 105 + 106 + test "returns 401 with invalid token" do 107 + conn = 108 + conn(:get, "/xrpc/dev.atvouch.graph.getCurrentUserVouches") 109 + |> put_req_header("authorization", "Bearer bad-token") 110 + |> Atvouch.Router.call(@opts) 111 + 112 + assert conn.status == 401 113 + end 114 + end 115 + 116 + describe "GET /xrpc/dev.atvouch.graph.getRemoteVouches" do 117 + test "returns vouches targeting the authenticated user" do 118 + # Alice has one vouch targeting her (from Bob) 119 + conn = 120 + conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 121 + |> put_req_header("authorization", "Bearer valid-token:did:plc:alice") 122 + |> Atvouch.Router.call(@opts) 123 + 124 + assert conn.status == 200 125 + body = Jason.decode!(conn.resp_body) 126 + assert is_list(body["vouches"]) 127 + assert length(body["vouches"]) == 1 128 + 129 + [vouch] = body["vouches"] 130 + assert vouch["uri"] == "at://did:plc:bob/dev.atvouch.graph.vouch/did:plc:alice" 131 + assert vouch["creatorDid"] == "did:plc:bob" 132 + assert vouch["targetDid"] == "did:plc:alice" 133 + end 134 + 135 + test "returns vouches targeting user with one voucher" do 136 + # Bob has one vouch targeting him (from Alice) 137 + conn = 138 + conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 139 + |> put_req_header("authorization", "Bearer valid-token:did:plc:bob") 140 + |> Atvouch.Router.call(@opts) 141 + 142 + assert conn.status == 200 143 + body = Jason.decode!(conn.resp_body) 144 + assert length(body["vouches"]) == 1 145 + 146 + [vouch] = body["vouches"] 147 + assert vouch["targetDid"] == "did:plc:bob" 148 + assert vouch["creatorDid"] == "did:plc:alice" 149 + end 150 + 151 + test "returns vouches targeting carol from alice" do 152 + # Carol has one vouch targeting her (from Alice) 153 + conn = 154 + conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 155 + |> put_req_header("authorization", "Bearer valid-token:did:plc:carol") 156 + |> Atvouch.Router.call(@opts) 157 + 158 + assert conn.status == 200 159 + body = Jason.decode!(conn.resp_body) 160 + assert length(body["vouches"]) == 1 161 + 162 + [vouch] = body["vouches"] 163 + assert vouch["creatorDid"] == "did:plc:alice" 164 + assert vouch["targetDid"] == "did:plc:carol" 165 + end 166 + 167 + test "returns 401 without authentication" do 168 + conn = 169 + conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 170 + |> Atvouch.Router.call(@opts) 171 + 172 + assert conn.status == 401 173 + end 174 + 175 + test "returns 401 with invalid token" do 176 + conn = 177 + conn(:get, "/xrpc/dev.atvouch.graph.getRemoteVouches") 178 + |> put_req_header("authorization", "Bearer bad-token") 179 + |> Atvouch.Router.call(@opts) 180 + 181 + assert conn.status == 401 182 + end 183 + end 184 + end
+60
appview/test/support/fake_auth_server.ex
··· 1 + defmodule Atvouch.Test.FakeAuthServer do 2 + @moduledoc false 3 + 4 + defmodule Router do 5 + use Plug.Router 6 + 7 + plug(:match) 8 + plug(:dispatch) 9 + 10 + get "/user" do 11 + case get_req_header(conn, "authorization") do 12 + ["Bearer " <> token] -> 13 + case token_to_user(token) do 14 + {:ok, did, aud} -> 15 + conn 16 + |> put_resp_content_type("application/json") 17 + |> send_resp(200, Jason.encode!(%{did: did, aud: aud})) 18 + 19 + :error -> 20 + conn 21 + |> put_resp_content_type("application/json") 22 + |> send_resp(401, Jason.encode!(%{error: "invalid_token"})) 23 + end 24 + 25 + _ -> 26 + conn 27 + |> put_resp_content_type("application/json") 28 + |> send_resp(401, Jason.encode!(%{error: "missing_authorization"})) 29 + end 30 + end 31 + 32 + match _ do 33 + send_resp(conn, 404, "not found") 34 + end 35 + 36 + # Token format: "valid-token:<did>" maps to a valid user 37 + defp token_to_user(token) do 38 + case String.split(token, ":", parts: 2) do 39 + ["valid-token", did] -> 40 + aud = Application.get_env(:atvouch, :atvouch_did) 41 + {:ok, did, aud} 42 + 43 + _ -> 44 + :error 45 + end 46 + end 47 + end 48 + 49 + def start do 50 + {:ok, server_pid} = 51 + Bandit.start_link( 52 + plug: Router, 53 + port: 0, 54 + ip: {127, 0, 0, 1} 55 + ) 56 + 57 + {:ok, {_ip, port}} = ThousandIsland.listener_info(server_pid) 58 + {server_pid, port} 59 + end 60 + end
+16
lexicons/dev/atvouch/graph/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.atvouch.graph.defs", 4 + "defs": { 5 + "vouchView": { 6 + "type": "object", 7 + "required": ["uri", "creatorDid", "targetDid", "createdAt"], 8 + "properties": { 9 + "uri": { "type": "string", "format": "at-uri" }, 10 + "creatorDid": { "type": "string", "format": "did" }, 11 + "targetDid": { "type": "string", "format": "did" }, 12 + "createdAt": { "type": "string", "format": "datetime" } 13 + } 14 + } 15 + } 16 + }
+26
lexicons/dev/atvouch/graph/getCurrentUserVouches.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.atvouch.graph.getCurrentUserVouches", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Returns all vouches created by the currently authenticated user.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["vouches"], 13 + "properties": { 14 + "vouches": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "dev.atvouch.graph.defs#vouchView" 19 + } 20 + } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }
+26
lexicons/dev/atvouch/graph/getRemoteVouches.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.atvouch.graph.getRemoteVouches", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Returns all vouches targeting the currently authenticated user.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["vouches"], 13 + "properties": { 14 + "vouches": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "dev.atvouch.graph.defs#vouchView" 19 + } 20 + } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }