Vow, uncensorable PDS written in Go

refactor: remove x402 support and ipfs pinning via x402

+1 -427
-2
cmd/vow/flags.go
··· 19 19 flagSmtpName = "smtp-name" 20 20 flagIpfsNodeUrl = "ipfs-node-url" 21 21 flagIpfsGatewayUrl = "ipfs-gateway-url" 22 - flagX402PinURL = "x402-pin-url" 23 - flagX402Network = "x402-network" 24 22 flagSessionSecret = "session-secret" 25 23 flagSessionCookieKey = "session-cookie-key" 26 24 flagFallbackProxy = "fallback-proxy"
-12
cmd/vow/main.go
··· 80 80 pf.String(flagSmtpName, "", "SMTP from name") 81 81 pf.String(flagIpfsNodeUrl, "http://127.0.0.1:5001", "Base URL of the Kubo RPC API (e.g. http://127.0.0.1:5001 or http://ipfs:5001 in Docker). All repo blocks and blobs are stored via this node") 82 82 pf.String(flagIpfsGatewayUrl, "", "Public IPFS gateway URL for blob redirects (e.g. http://localhost:8080). When set, sync.getBlob redirects to the gateway instead of proxying through vow") 83 - pf.String(flagX402PinURL, "", "x402-gated remote pinning endpoint (e.g. https://402.pinata.cloud/v1/pin/public). When set, accounts with x402 pinning enabled will have blobs pinned here after local storage, with payment signed by the user's Ethereum wallet") 84 - pf.String(flagX402Network, "eip155:8453", "CAIP-2 chain identifier required by the x402 pinning service (e.g. eip155:8453 for Base Mainnet)") 85 - 86 83 pf.String(flagSessionSecret, "", "Session secret") 87 84 pf.String(flagSessionCookieKey, "session", "Session cookie key name") 88 85 ··· 190 187 IPFSConfig: &server.IPFSConfig{ 191 188 NodeURL: v.GetString(flagIpfsNodeUrl), 192 189 GatewayURL: v.GetString(flagIpfsGatewayUrl), 193 - X402: func() *server.X402Config { 194 - if u := v.GetString(flagX402PinURL); u != "" { 195 - return &server.X402Config{ 196 - PinURL: u, 197 - Network: v.GetString(flagX402Network), 198 - } 199 - } 200 - return nil 201 - }(), 202 190 }, 203 191 SessionSecret: v.GetString(flagSessionSecret), 204 192 SessionCookieKey: v.GetString(flagSessionCookieKey),
-5
docker-compose.yaml
··· 82 82 VOW_IPFS_NODE_URL: ${VOW_IPFS_NODE_URL:-http://ipfs:5001} 83 83 # Optional public gateway for sync.getBlob redirects. 84 84 VOW_IPFS_GATEWAY_URL: ${VOW_IPFS_GATEWAY_URL:-} 85 - # Optional x402-gated remote pinning service. 86 - VOW_X402_PIN_URL: ${VOW_X402_PIN_URL:-} 87 - # CAIP-2 chain ID for x402 pinning. 88 - VOW_X402_NETWORK: ${VOW_X402_NETWORK:-eip155:8453} 89 - 90 85 # Optional fallback for proxied ATProto requests. 91 86 # Format: did#service-id, for example did:plc:xxx#atproto_labeler 92 87 VOW_FALLBACK_PROXY: ${VOW_FALLBACK_PROXY:-}
-6
go.mod
··· 5 5 require ( 6 6 github.com/bluesky-social/indigo v0.0.0-20260203235305-a86f3ae1f8ec 7 7 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 8 - github.com/coinbase/x402/go v0.0.0-20260309144830-34d2442cbf06 9 8 github.com/domodwyer/mailyak/v3 v3.6.2 10 9 github.com/ethereum/go-ethereum v1.17.1 11 10 github.com/glebarez/sqlite v1.11.0 ··· 39 38 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect 40 39 github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 41 40 github.com/beorn7/perks v1.0.1 // indirect 42 - github.com/bits-and-blooms/bitset v1.20.0 // indirect 43 41 github.com/cespare/xxhash/v2 v2.3.0 // indirect 44 - github.com/consensys/gnark-crypto v0.18.1 // indirect 45 - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect 46 42 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 47 43 github.com/dustin/go-humanize v1.0.1 // indirect 48 44 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 49 - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect 50 45 github.com/felixge/httpsnoop v1.0.4 // indirect 51 46 github.com/fsnotify/fsnotify v1.9.0 // indirect 52 47 github.com/glebarez/go-sqlite v1.21.2 // indirect ··· 121 116 github.com/spf13/cast v1.10.0 // indirect 122 117 github.com/spf13/pflag v1.0.10 // indirect 123 118 github.com/subosito/gotenv v1.6.0 // indirect 124 - github.com/supranational/blst v0.3.16 // indirect 125 119 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 126 120 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 127 121 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
-32
go.sum
··· 3 3 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= 4 4 github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU= 5 5 github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4= 6 - github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 7 - github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 8 6 github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5ljuFxkLGPNem5Ui+KBjFJzKg4Fv2fnxe4dvzpM= 9 7 github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA= 10 8 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= ··· 14 12 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 13 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 16 14 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 17 - github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= 18 - github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 19 15 github.com/bluesky-social/indigo v0.0.0-20260203235305-a86f3ae1f8ec h1:fubriMftMNEmb35sF07gDCsdUSEd0+EIDebt/+5oQRU= 20 16 github.com/bluesky-social/indigo v0.0.0-20260203235305-a86f3ae1f8ec/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= 21 17 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= ··· 24 20 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 25 21 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 26 22 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 27 - github.com/coinbase/x402/go v0.0.0-20260309144830-34d2442cbf06 h1:Ajrh7uBbhMNErMbrp7YZkz6+fkMVMg/ILla99uRAxuo= 28 - github.com/coinbase/x402/go v0.0.0-20260309144830-34d2442cbf06/go.mod h1:Igc3tBTV0bx8sK0s2zi0qNLO3M+jloH9nMuZx6t3r1Y= 29 - github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= 30 - github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= 31 23 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 32 24 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 33 - github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= 34 - github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= 35 25 github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 36 26 github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 37 27 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= ··· 47 37 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 48 38 github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 49 39 github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 50 - github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= 51 - github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 52 - github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= 53 - github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= 54 40 github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= 55 41 github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= 56 42 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 57 43 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 58 44 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 59 45 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 60 - github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= 61 - github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= 62 46 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 63 47 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 64 48 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= ··· 74 58 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 75 59 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 76 60 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 77 - github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 78 - github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 79 61 github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw= 80 62 github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec= 81 63 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= ··· 93 75 github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 94 76 github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= 95 77 github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= 96 - github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 97 - github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 98 78 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 99 79 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 100 80 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= ··· 245 225 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 246 226 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 247 227 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 248 - github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= 249 - github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= 250 228 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 251 229 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 252 230 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= ··· 290 268 github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 291 269 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 292 270 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 293 - github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 294 - github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 295 271 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 296 272 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 297 273 github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= ··· 347 323 github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= 348 324 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 349 325 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 350 - github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 351 - github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 352 326 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 353 327 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 354 328 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= ··· 379 353 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 380 354 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 381 355 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 382 - github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= 383 - github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 384 - github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 385 - github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 386 - github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 387 - github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 388 356 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 389 357 github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 390 358 github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
-5
models/models.go
··· 29 29 Root []byte 30 30 Preferences []byte 31 31 Deactivated bool 32 - // X402PinningEnabled controls whether blobs and repo blocks are 33 - // additionally pinned to a remote x402-gated pinning service after being 34 - // written to the local Kubo node. When false (the default) content lives 35 - // only on the co-located node. 36 - X402PinningEnabled bool `gorm:"default:false"` 37 32 } 38 33 39 34 // EthereumAddress returns the Ethereum address for PublicKey.
-22
server/handle_account_signer.go
··· 2 2 3 3 import ( 4 4 "encoding/base64" 5 - "encoding/hex" 6 5 "encoding/json" 7 6 "net/http" 8 - "strings" 9 7 "time" 10 8 11 9 "github.com/gorilla/websocket" ··· 128 126 case "sign_reject": 129 127 if !s.signerHub.DeliverRejection(did, in.RequestID) { 130 128 logger.Warn("signer: sign_reject for unknown requestId", "did", did, "requestId", in.RequestID) 131 - } 132 - 133 - case "pay_response": 134 - if in.Signature == "" { 135 - logger.Warn("signer: pay_response missing signature", "did", did) 136 - continue 137 - } 138 - hexStr := strings.TrimPrefix(in.Signature, "0x") 139 - sigBytes, err := hex.DecodeString(hexStr) 140 - if err != nil { 141 - logger.Warn("signer: pay_response bad hex", "did", did, "error", err) 142 - continue 143 - } 144 - if !s.signerHub.DeliverSignature(did, in.RequestID, sigBytes) { 145 - logger.Warn("signer: pay_response for unknown requestId", "did", did, "requestId", in.RequestID) 146 - } 147 - 148 - case "pay_reject": 149 - if !s.signerHub.DeliverRejection(did, in.RequestID) { 150 - logger.Warn("signer: pay_reject for unknown requestId", "did", did, "requestId", in.RequestID) 151 129 } 152 130 153 131 default:
-24
server/handle_repo_upload_blob.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "context" 6 5 "fmt" 7 6 "io" 8 7 "mime/multipart" ··· 70 69 logger.Error("error adding blob to ipfs", "error", err) 71 70 helpers.ServerError(w, nil) 72 71 return 73 - } 74 - 75 - // If the account has opted into x402 remote pinning and the signer 76 - // is connected, kick off the payment+pin flow in the 77 - // background. The blob is already safe on the local Kubo node so this 78 - // is best-effort — a failure here does not affect the ATProto response. 79 - if urepo.X402PinningEnabled && s.ipfsConfig.X402 != nil { 80 - walletAddr := urepo.EthereumAddress() 81 - if walletAddr == "" { 82 - logger.Warn("x402 pinning enabled but no public key registered; skipping", "cid", c.String()) 83 - } else if !s.signerHub.IsConnected(urepo.Repo.Did) { 84 - logger.Warn("x402 pinning enabled but signer not connected; skipping", "cid", c.String()) 85 - } else { 86 - cidStr := c.String() 87 - blobSize := read 88 - go func() { 89 - pinCtx, cancel := context.WithTimeout(context.Background(), 2*signerRequestTimeout) 90 - defer cancel() 91 - if err := s.pinBlobWithX402(pinCtx, urepo.Repo.Did, walletAddr, cidStr, blobSize); err != nil { 92 - logger.Warn("x402 remote pin failed", "cid", cidStr, "error", err) 93 - } 94 - }() 95 - } 96 72 } 97 73 98 74 // Persist a metadata row so we can list blobs by DID, resolve ownership,
+1 -34
server/handle_signer_connect.go
··· 2 2 3 3 import ( 4 4 "encoding/base64" 5 - "encoding/hex" 6 5 "encoding/json" 7 6 "net/http" 8 7 "strings" ··· 42 41 } 43 42 44 43 // wsIncoming is used for initial type-sniffing before full decode. 45 - // It covers both commit signing (sign_response / sign_reject) and 46 - // x402 payment signing (pay_response / pay_reject). 47 44 type wsIncoming struct { 48 45 Type string `json:"type"` 49 46 RequestID string `json:"requestId"` 50 - // sign_response: base64url-encoded EIP-191 signature bytes. 47 + // sign_response: base64url-encoded signature bytes. 51 48 Signature string `json:"signature,omitempty"` 52 - // pay_response: 0x-prefixed hex-encoded EIP-712 signature returned by 53 - // eth_signTypedData_v4. The PDS passes these bytes directly to the x402 54 - // SDK to assemble the final PaymentPayload. 55 - // Note: the field is also named "signature" in the pay_response JSON so 56 - // that the signer can use a single builder function; we distinguish the 57 - // two cases by message type. 58 49 } 59 50 60 51 // handleSignerConnect upgrades the connection to a WebSocket and registers it ··· 215 206 case in := <-inbound: 216 207 switch in.Type { 217 208 case "sign_response": 218 - // signature is base64url-encoded EIP-191 bytes. 219 209 if in.Signature == "" { 220 210 logger.Warn("signer: sign_response missing signature", "did", did) 221 211 continue ··· 232 222 case "sign_reject": 233 223 if !s.signerHub.DeliverRejection(did, in.RequestID) { 234 224 logger.Warn("signer: sign_reject for unknown requestId", "did", did, "requestId", in.RequestID) 235 - } 236 - 237 - case "pay_response": 238 - // signature is the 0x-prefixed hex-encoded EIP-712 signature 239 - // returned by eth_signTypedData_v4. The x402 SDK receives these 240 - // raw bytes and assembles the PaymentPayload itself. 241 - if in.Signature == "" { 242 - logger.Warn("signer: pay_response missing signature", "did", did) 243 - continue 244 - } 245 - hexStr := strings.TrimPrefix(in.Signature, "0x") 246 - sigBytes, err := hex.DecodeString(hexStr) 247 - if err != nil { 248 - logger.Warn("signer: pay_response bad hex", "did", did, "error", err) 249 - continue 250 - } 251 - if !s.signerHub.DeliverSignature(did, in.RequestID, sigBytes) { 252 - logger.Warn("signer: pay_response for unknown requestId", "did", did, "requestId", in.RequestID) 253 - } 254 - 255 - case "pay_reject": 256 - if !s.signerHub.DeliverRejection(did, in.RequestID) { 257 - logger.Warn("signer: pay_reject for unknown requestId", "did", did, "requestId", in.RequestID) 258 225 } 259 226 260 227 default:
-264
server/ipfs.go
··· 1 1 package server 2 2 3 3 import ( 4 - "bytes" 5 4 "context" 6 5 "encoding/json" 7 - "fmt" 8 6 "io" 9 7 "net/http" 10 - "time" 11 - 12 - x402 "github.com/coinbase/x402/go" 13 - x402http "github.com/coinbase/x402/go/http" 14 - x402evm "github.com/coinbase/x402/go/mechanisms/evm" 15 - evmexactclient "github.com/coinbase/x402/go/mechanisms/evm/exact/client" 16 - "github.com/google/uuid" 17 8 ) 18 9 19 10 // readJSON decodes a single JSON value from r into dst. ··· 21 12 return json.NewDecoder(r).Decode(dst) 22 13 } 23 14 24 - // ── signerHubEvmSigner ──────────────────────────────────────────────────────── 25 - 26 - // signerHubEvmSigner implements x402evm.ClientEvmSigner by delegating 27 - // SignTypedData to the user's Ethereum wallet via the signer WebSocket. 28 - // This means the PDS never holds a private key — the EIP-712 payload is 29 - // forwarded to the signer as a pay_request message and the resulting 30 - // 65-byte signature is returned through the SignerHub. 31 - type signerHubEvmSigner struct { 32 - hub *SignerHub 33 - did string 34 - address string // EIP-55 checksummed Ethereum address 35 - } 36 - 37 - // Address returns the EIP-55 checksummed Ethereum address of the signer, 38 - // derived from the account's stored secp256k1 public key. 39 - func (s *signerHubEvmSigner) Address() string { 40 - return s.address 41 - } 42 - 43 - // SignTypedData sends the EIP-712 typed data to the signer as a pay_request 44 - // WebSocket message and blocks until the signer returns the 65-byte signature 45 - // via pay_response, or until the context is cancelled. 46 - // 47 - // The typed data is serialised to JSON and forwarded verbatim so the signer 48 - // can pass it directly to eth_signTypedData_v4 without any transformation. 49 - func (s *signerHubEvmSigner) SignTypedData( 50 - ctx context.Context, 51 - domain x402evm.TypedDataDomain, 52 - types map[string][]x402evm.TypedDataField, 53 - primaryType string, 54 - message map[string]any, 55 - ) ([]byte, error) { 56 - // Serialise the full EIP-712 object so the signer can call 57 - // eth_signTypedData_v4(walletAddress, JSON.stringify(typedData)). 58 - typedDataPayload := map[string]any{ 59 - "domain": domain, 60 - "types": types, 61 - "primaryType": primaryType, 62 - "message": message, 63 - } 64 - typedDataJSON, err := json.Marshal(typedDataPayload) 65 - if err != nil { 66 - return nil, fmt.Errorf("x402: marshalling typed data: %w", err) 67 - } 68 - 69 - requestID := uuid.NewString() 70 - expiresAt := time.Now().Add(signerRequestTimeout) 71 - 72 - // Build a human-readable description from the typed data message fields 73 - // so the signer can show it in the notification before prompting. 74 - description := buildPayDescription(domain, message) 75 - 76 - msg, err := json.Marshal(wsPayRequest{ 77 - Type: "pay_request", 78 - RequestID: requestID, 79 - Did: s.did, 80 - TypedData: typedDataJSON, 81 - WalletAddress: s.address, 82 - Description: description, 83 - ExpiresAt: expiresAt.UTC().Format(time.RFC3339), 84 - }) 85 - if err != nil { 86 - return nil, fmt.Errorf("x402: encoding pay_request: %w", err) 87 - } 88 - 89 - signCtx, cancel := context.WithDeadline(ctx, expiresAt) 90 - defer cancel() 91 - 92 - // RequestSignature blocks until the signer returns the signature bytes 93 - // (delivered via pay_response) or the context times out / user rejects. 94 - return s.hub.RequestSignature(signCtx, s.did, requestID, msg) 95 - } 96 - 97 - // ReadContract is not implemented — the PDS has no RPC connection and the 98 - // x402 EIP-3009 flow does not require on-chain reads on the client side. 99 - func (s *signerHubEvmSigner) ReadContract( 100 - _ context.Context, 101 - _ string, 102 - _ []byte, 103 - _ string, 104 - _ ...any, 105 - ) (any, error) { 106 - return nil, fmt.Errorf("x402: ReadContract not supported on keyless PDS") 107 - } 108 - 109 - // ── wsPayRequest ────────────────────────────────────────────────────────────── 110 - 111 - // wsPayRequest is the WebSocket message the PDS sends to the signer when it 112 - // needs the user's wallet to sign an EIP-712 typed-data payload for an x402 113 - // EIP-3009 payment authorisation. 114 - type wsPayRequest struct { 115 - Type string `json:"type"` // always "pay_request" 116 - // RequestID is a UUID echoed back in the pay_response so the SignerHub 117 - // can route the reply to the correct waiting goroutine. 118 - RequestID string `json:"requestId"` 119 - Did string `json:"did"` 120 - // TypedData is the full EIP-712 object (domain, types, primaryType, 121 - // message) as produced by the x402 SDK. The signer passes it verbatim 122 - // to eth_signTypedData_v4(walletAddress, JSON.stringify(typedData)). 123 - TypedData json.RawMessage `json:"typedData"` 124 - // WalletAddress is the EIP-55 checksummed Ethereum address of the payer, 125 - // derived from the account's stored public key. Passed to the wallet as 126 - // the first argument of eth_signTypedData_v4. 127 - WalletAddress string `json:"walletAddress"` 128 - // Description is a human-readable summary shown in the signer's 129 - // notification before the wallet prompt appears, e.g. 130 - // "Pin blob bafyrei… (12 KB) via x402 on eip155:8453". 131 - Description string `json:"description,omitempty"` 132 - ExpiresAt string `json:"expiresAt"` // RFC3339 133 - } 134 - 135 - // ── x402 HTTP client factory ────────────────────────────────────────────────── 136 - 137 - // newX402HTTPClient builds an *http.Client that transparently handles the x402 138 - // 402-payment handshake for the given account. On a 402 response it: 139 - // 140 - // 1. Parses the payment requirements using the x402 SDK. 141 - // 2. Calls signerHubEvmSigner.SignTypedData, which sends a pay_request over 142 - // the account's signer WebSocket and waits for the wallet signature. 143 - // 3. Encodes the signed PaymentPayload and retries the original request with 144 - // the X-PAYMENT / PAYMENT-SIGNATURE header set. 145 - func (s *Server) newX402HTTPClient(did, walletAddress string) *http.Client { 146 - signer := &signerHubEvmSigner{ 147 - hub: s.signerHub, 148 - did: did, 149 - address: walletAddress, 150 - } 151 - 152 - x402Client := x402.Newx402Client(). 153 - Register( 154 - x402.Network(s.ipfsConfig.X402.Network), 155 - evmexactclient.NewExactEvmScheme(signer), 156 - ) 157 - 158 - return x402http.WrapHTTPClientWithPayment( 159 - &http.Client{Timeout: 2 * signerRequestTimeout}, 160 - x402http.Newx402HTTPClient(x402Client), 161 - ) 162 - } 163 - 164 - // ── request / response types ────────────────────────────────────────────────── 165 - 166 - // x402PinRequest is the JSON body posted to the x402 pinning endpoint. 167 - // Pinata's server requires the file size upfront so it can compute a dynamic 168 - // price before the file is transferred. 169 - type x402PinRequest struct { 170 - FileSize int `json:"fileSize"` 171 - } 172 - 173 - // x402PinResponse is the success body returned by the pinning endpoint. 174 - // Pinata returns a presigned upload URL the caller can use to push content 175 - // directly without an API key. 176 - type x402PinResponse struct { 177 - URL string `json:"url"` 178 - } 179 - 180 - // ── pinBlobWithX402 ─────────────────────────────────────────────────────────── 181 - 182 - // pinBlobWithX402 pins cidStr to the configured x402 pinning endpoint on 183 - // behalf of the account identified by did / walletAddress. The end-to-end flow: 184 - // 185 - // 1. POST the file size to cfg.PinURL. 186 - // 2. The x402 HTTP transport intercepts the 402 response, selects a payment 187 - // requirement, and calls signerHubEvmSigner.SignTypedData. 188 - // 3. SignTypedData sends a pay_request WebSocket message to the signer and 189 - // blocks until the wallet returns the EIP-712 signature. 190 - // 4. The transport re-encodes the signed PaymentPayload and retries the POST 191 - // with the appropriate payment header. 192 - // 5. On success the presigned URL (if returned) is logged. 193 - // 194 - // The call is intentionally non-fatal: the blob is already safe on the local 195 - // Kubo node. Callers should log any returned error but not propagate it as an 196 - // ATProto failure. 197 - func (s *Server) pinBlobWithX402(ctx context.Context, did, walletAddress, cidStr string, fileSize int) error { 198 - cfg := s.ipfsConfig.X402 199 - if cfg == nil || cfg.PinURL == "" { 200 - return fmt.Errorf("x402 pinning not configured") 201 - } 202 - 203 - logger := s.logger.With("op", "x402Pin", "did", did, "cid", cidStr) 204 - 205 - body, err := json.Marshal(x402PinRequest{FileSize: fileSize}) 206 - if err != nil { 207 - return fmt.Errorf("x402: encoding pin request: %w", err) 208 - } 209 - 210 - req, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.PinURL, bytes.NewReader(body)) 211 - if err != nil { 212 - return fmt.Errorf("x402: building pin request: %w", err) 213 - } 214 - req.Header.Set("Content-Type", "application/json") 215 - // Provide GetBody so the x402 transport can replay the body on the 216 - // payment-retry request without consuming the original reader twice. 217 - req.GetBody = func() (io.ReadCloser, error) { 218 - return io.NopCloser(bytes.NewReader(body)), nil 219 - } 220 - 221 - httpClient := s.newX402HTTPClient(did, walletAddress) 222 - 223 - resp, err := httpClient.Do(req) 224 - if err != nil { 225 - return fmt.Errorf("x402: pin request failed: %w", err) 226 - } 227 - defer func() { _ = resp.Body.Close() }() 228 - 229 - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { 230 - msg, _ := io.ReadAll(resp.Body) 231 - return fmt.Errorf("x402: pin rejected (status %d): %s", resp.StatusCode, string(msg)) 232 - } 233 - 234 - var pinResp x402PinResponse 235 - if err := readJSON(resp.Body, &pinResp); err == nil && pinResp.URL != "" { 236 - logger.Info("x402 pin accepted", "presignedURL", pinResp.URL) 237 - } else { 238 - logger.Info("x402 pin accepted") 239 - } 240 - 241 - return nil 242 - } 243 - 244 - // ── unpinFromIPFS ───────────────────────────────────────────────────────────── 245 - 246 15 // unpinFromIPFS asks the local Kubo node to remove the recursive pin for the 247 16 // given CID so the content becomes eligible for garbage collection. 248 17 // It is intentionally best-effort: errors are logged but not propagated. ··· 276 45 277 46 s.logger.Info("ipfs unpin: blob unpinned", "cid", cidStr) 278 47 } 279 - 280 - // ── compile-time interface assertions ───────────────────────────────────────── 281 - 282 - // Ensure signerHubEvmSigner satisfies the x402 ClientEvmSigner interface so 283 - // that build failures surface here rather than deep in the x402 SDK internals. 284 - var _ x402evm.ClientEvmSigner = (*signerHubEvmSigner)(nil) 285 - 286 - // buildPayDescription creates a short human-readable description of the 287 - // payment for use in the signer notification. It extracts the token value 288 - // and recipient from the EIP-712 message fields, falling back to a generic 289 - // string if the fields are missing or unparseable. 290 - func buildPayDescription(domain x402evm.TypedDataDomain, message map[string]any) string { 291 - value, _ := message["value"].(string) 292 - to, _ := message["to"].(string) 293 - 294 - chainID := "" 295 - if domain.ChainID != nil { 296 - chainID = fmt.Sprintf("eip155:%s", domain.ChainID.String()) 297 - } 298 - 299 - if value != "" && to != "" && chainID != "" { 300 - // Truncate the recipient address for display. 301 - toShort := to 302 - if len(to) > 10 { 303 - toShort = to[:6] + "…" + to[len(to)-4:] 304 - } 305 - return fmt.Sprintf("x402 payment: %s units → %s on %s", value, toShort, chainID) 306 - } 307 - if value != "" && chainID != "" { 308 - return fmt.Sprintf("x402 payment: %s units on %s", value, chainID) 309 - } 310 - return "Authorise an x402 payment?" 311 - }
-21
server/server.go
··· 51 51 // alongside. All repo blocks and blob data are stored on and retrieved from 52 52 // the co-located Kubo node — SQLite is used only for relational metadata 53 53 // (accounts, sessions, records index, etc.), not for content. 54 - // X402Config holds the configuration for the optional x402-gated remote 55 - // pinning service. When set, accounts that have opted in will have their 56 - // blobs pinned there after being written to the local Kubo node, with the 57 - // payment authorised by the user's Ethereum wallet via the browser-based signer. 58 - type X402Config struct { 59 - // PinURL is the base URL of the x402-gated pinning endpoint, 60 - // e.g. "https://402.pinata.cloud/v1/pin/public". The PDS POSTs to this 61 - // URL with the blob size, receives a 402 with payment requirements, asks 62 - // the signer to sign the EIP-3009 payment authorisation, then retries 63 - // the request with the X-PAYMENT header. 64 - PinURL string 65 - 66 - // Network is the CAIP-2 chain identifier required by the pinning service, 67 - // e.g. "eip155:8453" for Base Mainnet. 68 - Network string 69 - } 70 - 71 54 type IPFSConfig struct { 72 55 // NodeURL is the base URL of the Kubo RPC API, e.g. "http://ipfs:5001" 73 56 // in Docker or "http://127.0.0.1:5001" locally. ··· 77 60 // "http://ipfs:8080" or "https://ipfs.io". When set, sync.getBlob 78 61 // redirects clients to the gateway instead of proxying through vow. 79 62 GatewayURL string 80 - 81 - // X402 is optional. When non-nil, accounts with X402PinningEnabled=true 82 - // will have their content additionally pinned via the x402 protocol. 83 - X402 *X402Config 84 63 } 85 64 86 65 type Server struct {