Monorepo for Tangled
0

Configure Feed

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

spindle: ingest repos and collaborators via tap

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (May 12, 2026, 11:59 AM +0300) commit ad9d6e45 parent b66498b9 change-id lsrtntun
+673 -247
+22
go.mod
··· 70 70 github.com/Microsoft/go-winio v0.6.2 // indirect 71 71 github.com/ProtonMail/go-crypto v1.3.0 // indirect 72 72 github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect 73 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 73 74 github.com/alecthomas/repr v0.5.2 // indirect 74 75 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 75 76 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect ··· 141 142 github.com/go-test/deep v1.1.1 // indirect 142 143 github.com/goccy/go-json v0.10.5 // indirect 143 144 github.com/gogo/protobuf v1.3.2 // indirect 145 + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 144 146 github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 145 147 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 146 148 github.com/golang/mock v1.6.0 // indirect ··· 163 165 github.com/ipfs/bbloom v0.0.4 // indirect 164 166 github.com/ipfs/boxo v0.36.0 // indirect 165 167 github.com/ipfs/go-block-format v0.2.3 // indirect 168 + github.com/ipfs/go-blockservice v0.5.2 // indirect 166 169 github.com/ipfs/go-datastore v0.9.0 // indirect 167 170 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 168 171 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 172 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 173 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 169 174 github.com/ipfs/go-ipld-cbor v0.2.1 // indirect 170 175 github.com/ipfs/go-ipld-format v0.6.3 // indirect 176 + github.com/ipfs/go-ipld-legacy v0.2.2 // indirect 171 177 github.com/ipfs/go-log v1.0.5 // indirect 172 178 github.com/ipfs/go-log/v2 v2.9.1 // indirect 179 + github.com/ipfs/go-merkledag v0.11.0 // indirect 173 180 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 181 + github.com/ipfs/go-verifcid v0.0.3 // indirect 182 + github.com/ipld/go-car v0.6.2 // indirect 183 + github.com/ipld/go-codec-dagpb v1.7.0 // indirect 184 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 174 185 github.com/jackc/pgpassfile v1.0.0 // indirect 175 186 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 176 187 github.com/jackc/puddle/v2 v2.2.2 // indirect 188 + github.com/jinzhu/inflection v1.0.0 // indirect 189 + github.com/jinzhu/now v1.1.5 // indirect 177 190 github.com/json-iterator/go v1.1.12 // indirect 178 191 github.com/kevinburke/ssh_config v1.2.0 // indirect 179 192 github.com/klauspost/compress v1.18.0 // indirect 180 193 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 194 + github.com/labstack/echo/v4 v4.11.3 // indirect 195 + github.com/labstack/gommon v0.4.1 // indirect 181 196 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 197 + github.com/mattn/go-colorable v0.1.14 // indirect 182 198 github.com/mattn/go-isatty v0.0.20 // indirect 183 199 github.com/mattn/go-runewidth v0.0.16 // indirect 184 200 github.com/minio/sha256-simd v1.0.1 // indirect ··· 209 225 github.com/prometheus/client_model v0.6.2 // indirect 210 226 github.com/prometheus/common v0.67.5 // indirect 211 227 github.com/prometheus/procfs v0.19.2 // indirect 228 + github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect 212 229 github.com/rivo/uniseg v0.4.7 // indirect 213 230 github.com/ryanuber/go-glob v1.0.0 // indirect 214 231 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 217 234 github.com/tidwall/match v1.2.0 // indirect 218 235 github.com/tidwall/pretty v1.2.1 // indirect 219 236 github.com/tidwall/sjson v1.2.5 // indirect 237 + github.com/valyala/bytebufferpool v1.0.0 // indirect 238 + github.com/valyala/fasttemplate v1.2.2 // indirect 220 239 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 221 240 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 222 241 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect ··· 242 261 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 243 262 gopkg.in/warnings.v0 v0.1.2 // indirect 244 263 gopkg.in/yaml.v2 v2.4.0 // indirect 264 + gorm.io/driver/postgres v1.6.0 // indirect 265 + gorm.io/driver/sqlite v1.6.0 // indirect 266 + gorm.io/gorm v1.31.1 // indirect 245 267 gotest.tools/v3 v3.5.2 // indirect 246 268 lukechampine.com/blake3 v1.4.1 // indirect 247 269 )
+96
go.sum
··· 12 12 github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 13 13 github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= 14 14 github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= 15 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU= 16 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4= 15 17 github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= 16 18 github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= 17 19 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= ··· 67 69 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 68 70 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 69 71 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 72 + github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= 73 + github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 70 74 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 71 75 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 72 76 github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= ··· 169 173 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 170 174 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 171 175 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 176 + github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 177 + github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 172 178 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 173 179 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 174 180 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 175 181 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 176 182 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 177 183 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 184 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 185 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 178 186 github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 179 187 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 180 188 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= ··· 206 214 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 207 215 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 208 216 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 217 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 218 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 209 219 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 210 220 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 211 221 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= ··· 240 250 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 241 251 github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= 242 252 github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= 253 + github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 254 + github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 243 255 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 244 256 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 245 257 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= ··· 254 266 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 255 267 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 256 268 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 269 + github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 270 + github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 257 271 github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 258 272 github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 259 273 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= ··· 336 350 github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= 337 351 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 338 352 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 353 + github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 354 + github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 339 355 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 340 356 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 341 357 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 342 358 github.com/ipfs/boxo v0.36.0 h1:DarrMBM46xCs6GU6Vz+AL8VUyXykqHAqZYx8mR0Oics= 343 359 github.com/ipfs/boxo v0.36.0/go.mod h1:92hnRXfP5ScKEIqlq9Ns7LR1dFXEVADKWVGH0fjk83k= 360 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 361 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 344 362 github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= 345 363 github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= 364 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 365 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 346 366 github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= 347 367 github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= 348 368 github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= ··· 351 371 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 352 372 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 353 373 github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 374 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 375 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 376 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 377 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 354 378 github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 355 379 github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 380 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 381 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 382 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 383 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 384 + github.com/ipfs/go-ipfs-pq v0.0.4 h1:U7jjENWJd1jhcrR8X/xHTaph14PTAK9O+yaLJbjqgOw= 385 + github.com/ipfs/go-ipfs-pq v0.0.4/go.mod h1:9UdLOIIb99IFrgT0Fc53pvbvlJBhpUb4GJuAQf3+O2A= 386 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 387 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 356 388 github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 357 389 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 358 390 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 359 391 github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= 360 392 github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8= 361 393 github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk= 394 + github.com/ipfs/go-ipld-legacy v0.2.2 h1:DThbqCPVLpWBcGtU23KDLiY2YRZZnTkXQyfz8aOfBkQ= 395 + github.com/ipfs/go-ipld-legacy v0.2.2/go.mod h1:hhkj+b3kG9b2BcUNw8IFYAsfeNo8E3U7eYlWeAOPyDU= 362 396 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 363 397 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 364 398 github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 365 399 github.com/ipfs/go-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk= 366 400 github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= 401 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 402 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 367 403 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 368 404 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 405 + github.com/ipfs/go-peertaskqueue v0.8.3 h1:tBPpGJy+A92RqtRFq5amJn0Uuj8Pw8tXi0X3eHfHM8w= 406 + github.com/ipfs/go-peertaskqueue v0.8.3/go.mod h1:OqVync4kPOcXEGdj/LKvox9DCB5mkSBeXsPczCxLtYA= 407 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 408 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 409 + github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= 410 + github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= 411 + github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0= 412 + github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM= 413 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 414 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 369 415 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 370 416 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 371 417 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= ··· 374 420 github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= 375 421 github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 376 422 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 423 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 424 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 425 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 426 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 427 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 428 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 429 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 430 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 377 431 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 378 432 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 379 433 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= ··· 387 441 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 388 442 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 389 443 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 444 + github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= 445 + github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= 390 446 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 391 447 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 392 448 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= ··· 397 453 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 398 454 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 399 455 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 456 + github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= 457 + github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= 458 + github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= 459 + github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= 460 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 461 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 462 + github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= 463 + github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= 464 + github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= 465 + github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= 466 + github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= 467 + github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= 468 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 469 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 470 + github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= 471 + github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= 472 + github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= 473 + github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= 400 474 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 401 475 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 402 476 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= ··· 438 512 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 439 513 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 440 514 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 515 + github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= 516 + github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= 517 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 518 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 441 519 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 442 520 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 521 + github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= 522 + github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= 443 523 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 444 524 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 525 + github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= 526 + github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= 445 527 github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= 446 528 github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= 447 529 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= ··· 506 588 github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= 507 589 github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= 508 590 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 591 + github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0= 592 + github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= 509 593 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 510 594 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 511 595 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= ··· 565 649 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 566 650 github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= 567 651 github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 652 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 653 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 654 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 655 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 568 656 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 569 657 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 570 658 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= ··· 572 660 github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 573 661 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 574 662 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 663 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 664 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 575 665 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 576 666 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 577 667 github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= ··· 809 899 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 810 900 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 811 901 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 902 + gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= 903 + gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= 904 + gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= 905 + gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= 906 + gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= 907 + gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 812 908 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 813 909 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 814 910 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+10
spindle/config/config.go
··· 13 13 DBPath string `env:"DB_PATH, default=spindle.db"` 14 14 Hostname string `env:"HOSTNAME, required"` 15 15 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 16 + Tap Tap `env:",prefix=TAP_"` 16 17 PlcUrl string `env:"PLC_URL, default=https://plc.directory"` 17 18 Dev bool `env:"DEV, default=false"` 18 19 Owner string `env:"OWNER, required"` ··· 21 22 QueueSize int `env:"QUEUE_SIZE, default=100"` 22 23 MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of pipelines that run at a time 23 24 MaxConcurrentWorkflows int `env:"MAX_CONCURRENT_WORKFLOWS, default=8"` // max number of workflow containers running at once (memory cap) 25 + } 26 + 27 + type Tap struct { 28 + Embed bool `env:"EMBED, default=true"` 29 + Url string `env:"URL, default=http://[::1]:2480"` 30 + Bind string `env:"BIND, default=[::1]:2480"` 31 + DBPath string `env:"DB_PATH, default=tap.db"` 32 + RelayUrl string `env:"RELAY_URL, default=https://bsky.network"` 33 + AdminPassword string `env:"ADMIN_PASSWORD"` 24 34 } 25 35 26 36 func (s Server) Did() syntax.DID {
+127
spindle/embedtap.go
··· 1 + package spindle 2 + 3 + import ( 4 + "context" 5 + "crypto/rand" 6 + "encoding/hex" 7 + "errors" 8 + "fmt" 9 + "log/slog" 10 + "net" 11 + "net/http" 12 + "strings" 13 + "time" 14 + 15 + "github.com/bluesky-social/indigo/service/tap" 16 + "tangled.org/core/api/tangled" 17 + "tangled.org/core/spindle/config" 18 + ) 19 + 20 + func randomAdminPassword() (string, error) { 21 + var b [32]byte 22 + if _, err := rand.Read(b[:]); err != nil { 23 + return "", fmt.Errorf("generate tap admin password: %w", err) 24 + } 25 + return hex.EncodeToString(b[:]), nil 26 + } 27 + 28 + func assertLoopbackBind(bind string) error { 29 + host, _, err := net.SplitHostPort(bind) 30 + if err != nil { 31 + return fmt.Errorf("parse tap bind %q: %w", bind, err) 32 + } 33 + if host == "" { 34 + return fmt.Errorf("embedded mode requires loopback host in tap bind %q", bind) 35 + } 36 + if strings.EqualFold(host, "localhost") { 37 + return nil 38 + } 39 + ip := net.ParseIP(host) 40 + if ip == nil || !ip.IsLoopback() { 41 + return fmt.Errorf("embedded tap bind %q must be loopback like 127.0.0.1 or ::1", bind) 42 + } 43 + return nil 44 + } 45 + 46 + type embeddedTap struct { 47 + tap *tap.Tap 48 + logger *slog.Logger 49 + } 50 + 51 + func startEmbeddedTap(ctx context.Context, cfg *config.Config, logger *slog.Logger) (*embeddedTap, error) { 52 + if err := assertLoopbackBind(cfg.Server.Tap.Bind); err != nil { 53 + return nil, err 54 + } 55 + 56 + tcfg := tap.Config{ 57 + DatabaseURL: "sqlite://" + cfg.Server.Tap.DBPath, 58 + DBMaxConns: 32, 59 + PLCURL: cfg.Server.PlcUrl, 60 + RelayUrl: cfg.Server.Tap.RelayUrl, 61 + FirehoseParallelism: 4, 62 + ResyncParallelism: 2, 63 + OutboxParallelism: 1, 64 + FirehoseCursorSaveInterval: time.Second, 65 + RepoFetchTimeout: 5 * time.Minute, 66 + IdentityCacheSize: 50_000, 67 + EventCacheSize: 10_000, 68 + SignalCollection: tangled.RepoNSID, 69 + CollectionFilters: []string{tangled.RepoNSID, tangled.RepoCollaboratorNSID}, 70 + AdminPassword: cfg.Server.Tap.AdminPassword, 71 + RetryTimeout: 60 * time.Second, 72 + } 73 + 74 + t, err := tap.New(tcfg) 75 + if err != nil { 76 + return nil, fmt.Errorf("tap.New: %w", err) 77 + } 78 + 79 + go func() { 80 + if err := t.Firehose.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { 81 + logger.Error("firehose terminated", "err", err) 82 + } 83 + }() 84 + t.Run(ctx) 85 + go func() { 86 + logger.Info("tap http server listening", "bind", cfg.Server.Tap.Bind) 87 + if err := t.Server.Start(cfg.Server.Tap.Bind); err != nil && !errors.Is(err, http.ErrServerClosed) { 88 + logger.Error("tap http server terminated", "err", err) 89 + } 90 + }() 91 + 92 + if err := waitForListener(ctx, cfg.Server.Tap.Bind, time.Now().Add(10*time.Second)); err != nil { 93 + logger.Warn("tap http server unreachable before timeout", "bind", cfg.Server.Tap.Bind, "err", err) 94 + } 95 + 96 + return &embeddedTap{tap: t, logger: logger}, nil 97 + } 98 + 99 + func waitForListener(ctx context.Context, addr string, deadline time.Time) error { 100 + if ctx.Err() != nil { 101 + return ctx.Err() 102 + } 103 + if time.Now().After(deadline) { 104 + return fmt.Errorf("timed out waiting for %s", addr) 105 + } 106 + c, err := net.DialTimeout("tcp", addr, 100*time.Millisecond) 107 + if err == nil { 108 + c.Close() 109 + return nil 110 + } 111 + time.Sleep(50 * time.Millisecond) 112 + return waitForListener(ctx, addr, deadline) 113 + } 114 + 115 + func (e *embeddedTap) Shutdown() { 116 + if e == nil || e.tap == nil { 117 + return 118 + } 119 + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 120 + defer cancel() 121 + if err := e.tap.Server.Shutdown(shutdownCtx); err != nil { 122 + e.logger.Error("tap server shutdown failed", "err", err) 123 + } 124 + if err := e.tap.CloseDb(shutdownCtx); err != nil { 125 + e.logger.Error("tap db close failed", "err", err) 126 + } 127 + }
-230
spindle/ingester.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "errors" 7 6 "fmt" 8 - "strings" 9 7 "time" 10 8 11 9 "tangled.org/core/api/tangled" 12 - "tangled.org/core/eventconsumer" 13 - "tangled.org/core/rbac" 14 10 "tangled.org/core/spindle/db" 15 11 16 - comatproto "github.com/bluesky-social/indigo/api/atproto" 17 - "github.com/bluesky-social/indigo/atproto/identity" 18 12 "github.com/bluesky-social/indigo/atproto/syntax" 19 - "github.com/bluesky-social/indigo/xrpc" 20 13 "github.com/bluesky-social/jetstream/pkg/models" 21 - securejoin "github.com/cyphar/filepath-securejoin" 22 14 ) 23 15 24 16 type Ingester func(ctx context.Context, e *models.Event) error ··· 33 25 switch e.Commit.Collection { 34 26 case tangled.SpindleMemberNSID: 35 27 err = s.ingestMember(ctx, e) 36 - case tangled.RepoNSID: 37 - err = s.ingestRepo(ctx, e) 38 - case tangled.RepoCollaboratorNSID: 39 - err = s.ingestCollaborator(ctx, e) 40 28 } 41 29 42 30 if err != nil { ··· 135 123 } 136 124 return nil 137 125 } 138 - 139 - func (s *Spindle) ingestRepo(ctx context.Context, e *models.Event) error { 140 - var err error 141 - did := e.Did 142 - 143 - l := s.l.With("component", "ingester", "record", tangled.RepoNSID) 144 - 145 - l.Info("ingesting repo record", "did", did) 146 - 147 - switch e.Commit.Operation { 148 - case models.CommitOperationCreate, models.CommitOperationUpdate: 149 - raw := e.Commit.Record 150 - record := tangled.Repo{} 151 - err = json.Unmarshal(raw, &record) 152 - if err != nil { 153 - l.Error("invalid record", "error", err) 154 - return err 155 - } 156 - 157 - domain := s.cfg.Server.Hostname 158 - rkey := e.Commit.RKey 159 - 160 - // no spindle configured for this repo 161 - if record.Spindle == nil { 162 - l.Info("no spindle configured", "rkey", rkey) 163 - return nil 164 - } 165 - 166 - // this repo did not want this spindle 167 - if *record.Spindle != domain { 168 - l.Info("different spindle configured", "rkey", rkey, "spindle", *record.Spindle, "domain", domain) 169 - return nil 170 - } 171 - 172 - // add this repo to the watch list 173 - if err := s.db.AddRepo(record.Knot, did, rkey); err != nil { 174 - l.Error("failed to add repo", "error", err) 175 - return fmt.Errorf("failed to add repo: %w", err) 176 - } 177 - 178 - didSlashRepo, err := securejoin.SecureJoin(did, rkey) 179 - if err != nil { 180 - return err 181 - } 182 - 183 - // add repo to rbac 184 - if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil { 185 - l.Error("failed to add repo to enforcer", "error", err) 186 - return fmt.Errorf("failed to add repo: %w", err) 187 - } 188 - 189 - // add collaborators to rbac 190 - owner, err := s.res.ResolveIdent(ctx, did) 191 - if err != nil || owner.Handle.IsInvalidHandle() { 192 - return err 193 - } 194 - if err := s.fetchAndAddCollaborators(ctx, owner, didSlashRepo); err != nil { 195 - return err 196 - } 197 - 198 - // add this knot to the event consumer 199 - src := eventconsumer.NewKnotSource(record.Knot) 200 - s.ks.AddSource(context.Background(), src) 201 - 202 - return nil 203 - 204 - } 205 - return nil 206 - } 207 - 208 - func (s *Spindle) ingestCollaborator(ctx context.Context, e *models.Event) error { 209 - var err error 210 - 211 - l := s.l.With("component", "ingester", "record", tangled.RepoCollaboratorNSID, "did", e.Did) 212 - 213 - l.Info("ingesting collaborator record") 214 - 215 - switch e.Commit.Operation { 216 - case models.CommitOperationCreate, models.CommitOperationUpdate: 217 - raw := e.Commit.Record 218 - record := tangled.RepoCollaborator{} 219 - err = json.Unmarshal(raw, &record) 220 - if err != nil { 221 - l.Error("invalid record", "error", err) 222 - return err 223 - } 224 - 225 - subjectId, err := s.res.ResolveIdent(ctx, record.Subject) 226 - if err != nil || subjectId.Handle.IsInvalidHandle() { 227 - return err 228 - } 229 - 230 - var rbacResource string 231 - var ownerDid string 232 - switch { 233 - case strings.HasPrefix(record.Repo, "did:"): 234 - resolvedOwner, repoName, lookupErr := s.resolveRepoDid(ctx, e.Did, record.Repo) 235 - if lookupErr != nil { 236 - return fmt.Errorf("unknown repo DID %s: %w", record.Repo, lookupErr) 237 - } 238 - ownerDid = resolvedOwner 239 - rbacResource, _ = securejoin.SecureJoin(ownerDid, repoName) 240 - 241 - case strings.Contains(record.Repo, "/"): 242 - repoAt, parseErr := syntax.ParseATURI(record.Repo) 243 - if parseErr != nil { 244 - l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo) 245 - return nil 246 - } 247 - 248 - owner, resolveErr := s.res.ResolveIdent(ctx, repoAt.Authority().String()) 249 - if resolveErr != nil || owner.Handle.IsInvalidHandle() { 250 - return fmt.Errorf("failed to resolve handle: %w", resolveErr) 251 - } 252 - 253 - xrpcc := xrpc.Client{ 254 - Host: owner.PDSEndpoint(), 255 - } 256 - 257 - resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 258 - if getErr != nil { 259 - return getErr 260 - } 261 - 262 - if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 263 - return fmt.Errorf("record at %s is not a tangled.Repo", repoAt) 264 - } 265 - rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repoAt.RecordKey().String()) 266 - ownerDid = owner.DID.String() 267 - 268 - default: 269 - l.Info("rejecting collaborator record with unrecognized repo format", "repo", record.Repo) 270 - return nil 271 - } 272 - 273 - if ok, err := s.e.IsCollaboratorInviteAllowed(ownerDid, rbac.ThisServer, rbacResource); !ok || err != nil { 274 - return fmt.Errorf("insufficient permissions: %w", err) 275 - } 276 - 277 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, rbacResource); err != nil { 278 - l.Error("failed to add collaborator to enforcer", "error", err) 279 - return fmt.Errorf("failed to add collaborator: %w", err) 280 - } 281 - 282 - return nil 283 - } 284 - return nil 285 - } 286 - 287 - func (s *Spindle) resolveRepoDid(ctx context.Context, ownerDid string, repoDid string) (string, string, error) { 288 - owner, resolveErr := s.res.ResolveIdent(ctx, ownerDid) 289 - if resolveErr != nil || owner.Handle.IsInvalidHandle() { 290 - return "", "", fmt.Errorf("failed to resolve owner %s: %w", ownerDid, resolveErr) 291 - } 292 - 293 - xrpcc := xrpc.Client{ 294 - Host: owner.PDSEndpoint(), 295 - } 296 - 297 - cursor := "" 298 - for { 299 - resp, listErr := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoNSID, cursor, 100, ownerDid, false) 300 - if listErr != nil { 301 - return "", "", fmt.Errorf("failed to list repo records for %s: %w", ownerDid, listErr) 302 - } 303 - 304 - for _, r := range resp.Records { 305 - if r == nil { 306 - continue 307 - } 308 - repo, ok := r.Value.Val.(*tangled.Repo) 309 - if !ok { 310 - continue 311 - } 312 - if repo.RepoDid != nil && *repo.RepoDid == repoDid { 313 - rkey := r.Uri[strings.LastIndex(r.Uri, "/")+1:] 314 - return ownerDid, rkey, nil 315 - } 316 - } 317 - 318 - if resp.Cursor == nil || *resp.Cursor == "" { 319 - break 320 - } 321 - cursor = *resp.Cursor 322 - } 323 - 324 - return "", "", fmt.Errorf("repo DID %s not found in records for %s", repoDid, ownerDid) 325 - } 326 - 327 - func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error { 328 - l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") 329 - 330 - l.Info("fetching and adding existing collaborators") 331 - 332 - xrpcc := xrpc.Client{ 333 - Host: owner.PDSEndpoint(), 334 - } 335 - 336 - resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false) 337 - if err != nil { 338 - return err 339 - } 340 - 341 - var errs error 342 - for _, r := range resp.Records { 343 - if r == nil { 344 - continue 345 - } 346 - record := r.Value.Val.(*tangled.RepoCollaborator) 347 - 348 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 349 - l.Error("failed to add repo to enforcer", "error", err) 350 - errors.Join(errs, fmt.Errorf("failed to add repo: %w", err)) 351 - } 352 - } 353 - 354 - return errs 355 - }
+70 -17
spindle/server.go
··· 10 10 "net/http" 11 11 "sync" 12 12 13 + "github.com/bluesky-social/indigo/atproto/syntax" 13 14 "github.com/go-chi/chi/v5" 14 15 "tangled.org/core/api/tangled" 15 16 "tangled.org/core/eventconsumer" ··· 39 40 40 41 type Spindle struct { 41 42 jc *jetstream.JetstreamClient 43 + tap *Tap 44 + embedTap *embeddedTap 42 45 db *db.DB 43 46 e *rbac.Enforcer 44 47 l *slog.Logger ··· 52 55 motd []byte 53 56 motdMu sync.RWMutex 54 57 workflowSem chan struct{} 58 + rootCtx context.Context 55 59 } 56 60 57 61 // New creates a new Spindle server with the provided configuration and engines. 58 62 func New(ctx context.Context, cfg *config.Config, engines map[string]models.Engine) (*Spindle, error) { 59 63 logger := log.FromContext(ctx) 60 64 61 - d, err := db.Make(cfg.Server.DBPath) 65 + d, err := db.Make(ctx, cfg.Server.DBPath) 62 66 if err != nil { 63 67 return nil, fmt.Errorf("failed to setup db: %w", err) 64 68 } ··· 104 108 105 109 collections := []string{ 106 110 tangled.SpindleMemberNSID, 107 - tangled.RepoNSID, 108 - tangled.RepoCollaboratorNSID, 109 111 } 110 112 jc, err := jetstream.NewJetstreamClient(cfg.Server.JetstreamEndpoint, "spindle", collections, nil, log.SubLogger(logger, "jetstream"), d, true, true) 111 113 if err != nil { ··· 137 139 vault: vault, 138 140 motd: defaultMotd, 139 141 workflowSem: workflowSem, 142 + rootCtx: ctx, 140 143 } 141 144 142 145 err = e.AddSpindle(rbacDomain) ··· 177 180 } 178 181 spindle.ks = eventconsumer.NewConsumer(*ccfg) 179 182 183 + if cfg.Server.Tap.Embed { 184 + pw, err := randomAdminPassword() 185 + if err != nil { 186 + return nil, err 187 + } 188 + cfg.Server.Tap.AdminPassword = pw 189 + logger.Info("embedded tap: using random admin password") 190 + } 191 + spindle.tap = NewTapClient(spindle) 192 + 180 193 return spindle, nil 181 194 } 182 195 ··· 235 248 defer stopper.Stop() 236 249 } 237 250 251 + if s.cfg.Server.Tap.Embed { 252 + emb, err := startEmbeddedTap(ctx, s.cfg, log.SubLogger(s.l, "embedtap")) 253 + if err != nil { 254 + return fmt.Errorf("starting embedded tap: %w", err) 255 + } 256 + s.embedTap = emb 257 + defer s.embedTap.Shutdown() 258 + } 259 + 238 260 go func() { 239 261 s.l.Info("starting knot event consumer") 240 262 s.ks.Start(ctx) 241 263 }() 242 264 265 + s.l.Info("starting tap client", "url", s.cfg.Server.Tap.Url) 266 + s.tap.Start(ctx) 267 + 243 268 s.l.Info("starting spindle server", "address", s.cfg.Server.ListenAddr) 244 269 return http.ListenAndServe(s.cfg.Server.ListenAddr, s.Router()) 245 270 } 246 271 272 + func (s *Spindle) declareTapInterest(ctx context.Context) { 273 + repos, err := s.db.AllRepos() 274 + if err != nil { 275 + s.l.Warn("tap declare: failed to load known repos", "err", err) 276 + return 277 + } 278 + seen := make(map[syntax.DID]struct{}, len(repos)) 279 + dids := make([]syntax.DID, 0, len(repos)) 280 + for _, r := range repos { 281 + if r.Owner == "" { 282 + continue 283 + } 284 + if _, ok := seen[r.Owner]; ok { 285 + continue 286 + } 287 + seen[r.Owner] = struct{}{} 288 + dids = append(dids, r.Owner) 289 + } 290 + if err := s.tap.AddOwnerDIDs(ctx, dids); err != nil { 291 + s.l.Warn("tap declare: AddRepos rejected", "count", len(dids), "err", err) 292 + return 293 + } 294 + s.l.Info("tap declare: known owner DIDs registered", "count", len(dids)) 295 + } 296 + 247 297 func Run(ctx context.Context) error { 248 298 cfg, err := config.Load(ctx) 249 299 if err != nil { ··· 319 369 return fmt.Errorf("repo knot does not match event source: %s != %s", src.Key(), tpl.TriggerMetadata.Repo.Knot) 320 370 } 321 371 322 - // filter by repos 323 - repoName := "" 324 - if tpl.TriggerMetadata.Repo.Repo != nil { 325 - repoName = *tpl.TriggerMetadata.Repo.Repo 326 - } 327 - 328 - _, err = s.db.GetRepo( 329 - tpl.TriggerMetadata.Repo.Knot, 330 - tpl.TriggerMetadata.Repo.Did, 331 - repoName, 332 - ) 372 + repoDid, err := s.resolvePipelineRepoDid(tpl.TriggerMetadata.Repo) 333 373 if err != nil { 334 - return fmt.Errorf("failed to get repo: %w", err) 374 + return err 335 375 } 336 376 337 377 pipelineId := models.PipelineId{ ··· 399 439 ok := s.jq.Enqueue(queue.Job{ 400 440 Run: func() error { 401 441 engine.StartWorkflows(log.SubLogger(s.l, "engine"), s.vault, s.cfg, s.db, s.n, s.workflowSem, ctx, &models.Pipeline{ 402 - RepoOwner: tpl.TriggerMetadata.Repo.Did, 403 - RepoName: repoName, 442 + RepoDid: repoDid, 404 443 Workflows: workflows, 405 444 }, pipelineId) 406 445 return nil ··· 417 456 } 418 457 419 458 return nil 459 + } 460 + 461 + func (s *Spindle) resolvePipelineRepoDid(repo *tangled.Pipeline_TriggerRepo) (syntax.DID, error) { 462 + if repo.RepoDid == nil || *repo.RepoDid == "" { 463 + return "", fmt.Errorf("pipeline trigger missing repoDid") 464 + } 465 + repoDid, err := syntax.ParseDID(*repo.RepoDid) 466 + if err != nil { 467 + return "", fmt.Errorf("parse repoDid %s: %w", *repo.RepoDid, err) 468 + } 469 + if _, err := s.db.GetRepoByDid(repoDid); err != nil { 470 + s.l.Warn("accepting knot pipeline assertion for unknown repoDid", "repoDid", repoDid, "err", err) 471 + } 472 + return repoDid, nil 420 473 } 421 474 422 475 func (s *Spindle) configureOwner() error {
+348
spindle/tapclient.go
··· 1 + package spindle 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "encoding/json" 7 + "errors" 8 + "fmt" 9 + "log/slog" 10 + "sync" 11 + "time" 12 + 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + "tangled.org/core/api/tangled" 15 + "tangled.org/core/eventconsumer" 16 + "tangled.org/core/log" 17 + "tangled.org/core/rbac" 18 + "tangled.org/core/spindle/db" 19 + "tangled.org/core/tapc" 20 + ) 21 + 22 + const ( 23 + maxPendingPerRepo = 64 24 + pendingCollabTTL = 10 * time.Minute 25 + ) 26 + 27 + type pendingCollabEvent struct { 28 + evt *tapc.RecordEventData 29 + at time.Time 30 + } 31 + 32 + type Tap struct { 33 + logger *slog.Logger 34 + spindle *Spindle 35 + tap tapc.Client 36 + pendingMu sync.Mutex 37 + pendingCollabs map[syntax.DID][]pendingCollabEvent 38 + } 39 + 40 + func NewTapClient(s *Spindle) *Tap { 41 + return &Tap{ 42 + logger: log.SubLogger(s.l, "tapclient"), 43 + spindle: s, 44 + tap: tapc.NewClient(s.cfg.Server.Tap.Url, s.cfg.Server.Tap.AdminPassword), 45 + pendingCollabs: make(map[syntax.DID][]pendingCollabEvent), 46 + } 47 + } 48 + 49 + func (t *Tap) AddOwnerDIDs(ctx context.Context, dids []syntax.DID) error { 50 + if len(dids) == 0 { 51 + return nil 52 + } 53 + return t.tap.AddRepos(ctx, dids) 54 + } 55 + 56 + func (t *Tap) Start(ctx context.Context) { 57 + go t.tap.Connect(ctx, &tapc.SimpleIndexer{ 58 + EventHandler: t.processEvent, 59 + ConnectHandler: t.onConnect, 60 + }) 61 + go t.purgePendingCollabsLoop(ctx) 62 + } 63 + 64 + func (t *Tap) onConnect(ctx context.Context) { 65 + t.spindle.declareTapInterest(ctx) 66 + } 67 + 68 + func (t *Tap) processEvent(ctx context.Context, evt tapc.Event) error { 69 + if evt.Type != tapc.EvtRecord || evt.Record == nil { 70 + return nil 71 + } 72 + switch evt.Record.Collection.String() { 73 + case tangled.RepoNSID: 74 + return t.processRepo(ctx, evt.Record) 75 + case tangled.RepoCollaboratorNSID: 76 + return t.processCollaborator(ctx, evt.Record) 77 + } 78 + return nil 79 + } 80 + 81 + func (t *Tap) processRepo(ctx context.Context, evt *tapc.RecordEventData) error { 82 + l := t.logger.With("collection", tangled.RepoNSID, "did", evt.Did, "rkey", evt.Rkey) 83 + 84 + ownerDid := evt.Did 85 + rkey := evt.Rkey 86 + 87 + switch evt.Action { 88 + case tapc.RecordCreateAction, tapc.RecordUpdateAction: 89 + record := tangled.Repo{} 90 + if err := json.Unmarshal(evt.Record, &record); err != nil { 91 + l.Warn("skipping invalid repo record", "err", err) 92 + return nil 93 + } 94 + 95 + hostname := t.spindle.cfg.Server.Hostname 96 + prior, priorErr := t.spindle.db.GetRepoByOwnerRkey(ownerDid, rkey) 97 + knownRepo := priorErr == nil 98 + 99 + if record.Spindle == nil || *record.Spindle != hostname { 100 + if knownRepo { 101 + l.Info("tearing down repo reassigned from this spindle", "newSpindle", record.Spindle) 102 + return t.teardownRepo(l, prior, ownerDid, rkey) 103 + } 104 + return nil 105 + } 106 + 107 + if record.RepoDid == nil || *record.RepoDid == "" { 108 + l.Warn("skipping repo record without repoDid") 109 + return nil 110 + } 111 + repoDid, err := syntax.ParseDID(*record.RepoDid) 112 + if err != nil { 113 + l.Warn("skipping repo record with malformed repoDid", "value", *record.RepoDid, "err", err) 114 + return nil 115 + } 116 + 117 + if err := t.spindle.e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()); err != nil { 118 + l.Error("failed to add repo policy", "err", err) 119 + return fmt.Errorf("add repo policy: %w", err) 120 + } 121 + 122 + src := eventconsumer.NewKnotSource(record.Knot) 123 + t.spindle.ks.AddSource(t.spindle.rootCtx, src) 124 + 125 + if err := t.spindle.db.AddRepo(db.Repo{ 126 + Knot: record.Knot, 127 + Owner: ownerDid, 128 + Rkey: rkey, 129 + RepoDid: repoDid, 130 + CreatedAt: record.CreatedAt, 131 + }); err != nil { 132 + l.Error("failed to add repo row", "err", err) 133 + return fmt.Errorf("add repo: %w", err) 134 + } 135 + 136 + if removed, err := t.spindle.db.CollapseRepoSiblings(ownerDid, repoDid); err != nil { 137 + l.Warn("collapse rename siblings failed", "err", err) 138 + } else if removed > 0 { 139 + l.Info("collapsed rename leftovers", "owner", ownerDid, "repo_did", repoDid, "removed", removed) 140 + } 141 + 142 + if err := t.tap.AddRepos(ctx, []syntax.DID{ownerDid}); err != nil { 143 + l.Warn("tap AddRepos rejected", "did", ownerDid, "err", err) 144 + } 145 + 146 + t.drainPendingCollabs(ctx, repoDid) 147 + 148 + case tapc.RecordDeleteAction: 149 + repo, err := t.spindle.db.GetRepoByOwnerRkey(ownerDid, rkey) 150 + if err != nil { 151 + l.Info("skipping delete for unknown repo") 152 + return nil 153 + } 154 + return t.teardownRepo(l, repo, ownerDid, rkey) 155 + } 156 + return nil 157 + } 158 + 159 + func (t *Tap) teardownRepo(l *slog.Logger, repo *db.Repo, ownerDid syntax.DID, rkey syntax.RecordKey) error { 160 + if repo.RepoDid != "" { 161 + collabs, err := t.spindle.db.ListCollaboratorsByRepoDid(repo.RepoDid) 162 + if err != nil { 163 + l.Error("failed to list collaborators for cleanup", "err", err) 164 + return fmt.Errorf("list collaborators: %w", err) 165 + } 166 + for _, c := range collabs { 167 + if err := t.spindle.e.RemoveCollaborator(c.Subject.String(), rbac.ThisServer, repo.RepoDid.String()); err != nil { 168 + l.Error("failed to remove collaborator policy", "subject", c.Subject, "err", err) 169 + return fmt.Errorf("remove collaborator policy: %w", err) 170 + } 171 + } 172 + if err := t.spindle.db.DeleteRepoCollaboratorsByRepoDid(repo.RepoDid); err != nil { 173 + l.Error("failed to clear collaborator rows", "err", err) 174 + return err 175 + } 176 + if err := t.spindle.e.RemoveRepo(ownerDid.String(), rbac.ThisServer, repo.RepoDid.String()); err != nil { 177 + l.Error("failed to remove repo policy", "err", err) 178 + return fmt.Errorf("remove repo policy: %w", err) 179 + } 180 + } 181 + if err := t.spindle.db.DeleteRepoByOwnerRkey(ownerDid, rkey); err != nil { 182 + l.Error("failed to delete repo row", "err", err) 183 + return fmt.Errorf("delete repo row: %w", err) 184 + } 185 + return nil 186 + } 187 + 188 + func (t *Tap) processCollaborator(ctx context.Context, evt *tapc.RecordEventData) error { 189 + l := t.logger.With("collection", tangled.RepoCollaboratorNSID, "did", evt.Did, "rkey", evt.Rkey) 190 + 191 + switch evt.Action { 192 + case tapc.RecordCreateAction, tapc.RecordUpdateAction: 193 + record := tangled.RepoCollaborator{} 194 + if err := json.Unmarshal(evt.Record, &record); err != nil { 195 + l.Warn("skipping invalid collaborator record", "err", err) 196 + return nil 197 + } 198 + 199 + actor := evt.Did 200 + rkey := evt.Rkey 201 + 202 + subjectDid, err := syntax.ParseDID(record.Subject) 203 + if err != nil { 204 + l.Info("skipping collaborator with malformed subject DID", "subject", record.Subject, "err", err) 205 + return nil 206 + } 207 + if _, err := t.spindle.res.ResolveIdent(ctx, subjectDid.String()); err != nil { 208 + l.Info("skipping unresolvable collaborator subject", "subject", subjectDid, "err", err) 209 + return nil 210 + } 211 + 212 + repoRefDid, err := syntax.ParseDID(record.Repo) 213 + if err != nil { 214 + l.Info("skipping collaborator with non-DID repo ref", "repo", record.Repo, "err", err) 215 + return nil 216 + } 217 + repo, lookupErr := t.spindle.db.GetRepoByDid(repoRefDid) 218 + if errors.Is(lookupErr, sql.ErrNoRows) { 219 + t.bufferCollab(repoRefDid, evt) 220 + l.Info("buffering collaborator until repo arrives", "repo", repoRefDid) 221 + return nil 222 + } 223 + if lookupErr != nil { 224 + return fmt.Errorf("lookup repo %s: %w", repoRefDid, lookupErr) 225 + } 226 + repoDid := repo.RepoDid 227 + ownerDid := repo.Owner 228 + 229 + if actor != ownerDid { 230 + l.Info("rejecting collaborator with non-owner actor", "actor", actor, "owner", ownerDid) 231 + return nil 232 + } 233 + 234 + ok, err := t.spindle.e.IsCollaboratorInviteAllowed(ownerDid.String(), rbac.ThisServer, repoDid.String()) 235 + if err != nil { 236 + l.Error("invite permission check failed", "err", err) 237 + return fmt.Errorf("invite check: %w", err) 238 + } 239 + if !ok { 240 + l.Info("rejecting collaborator invite", "owner", ownerDid, "repo", repoDid) 241 + return nil 242 + } 243 + 244 + prior, priorErr := t.spindle.db.GetRepoCollaborator(actor, rkey) 245 + staleSubject := priorErr == nil && (prior.Subject != subjectDid || prior.RepoDid != repoDid) 246 + 247 + if err := t.spindle.e.AddCollaborator(subjectDid.String(), rbac.ThisServer, repoDid.String()); err != nil { 248 + l.Error("failed to add collaborator policy", "err", err) 249 + return fmt.Errorf("add collaborator policy: %w", err) 250 + } 251 + if staleSubject { 252 + if err := t.spindle.e.RemoveCollaborator(prior.Subject.String(), rbac.ThisServer, prior.RepoDid.String()); err != nil { 253 + l.Error("failed to remove stale collaborator policy", "err", err) 254 + return fmt.Errorf("remove stale collaborator: %w", err) 255 + } 256 + } 257 + if err := t.spindle.db.AddRepoCollaborator(db.RepoCollaborator{ 258 + OwnerDid: actor, 259 + Rkey: rkey, 260 + Subject: subjectDid, 261 + RepoDid: repoDid, 262 + }); err != nil { 263 + l.Error("failed to persist collaborator row", "err", err) 264 + return fmt.Errorf("track collaborator: %w", err) 265 + } 266 + 267 + case tapc.RecordDeleteAction: 268 + actor := evt.Did 269 + rkey := evt.Rkey 270 + 271 + tracked, err := t.spindle.db.GetRepoCollaborator(actor, rkey) 272 + if err != nil { 273 + l.Info("skipping delete for unknown collaborator record") 274 + return nil 275 + } 276 + if err := t.spindle.e.RemoveCollaborator(tracked.Subject.String(), rbac.ThisServer, tracked.RepoDid.String()); err != nil { 277 + l.Error("failed to remove collaborator policy", "err", err) 278 + return fmt.Errorf("remove collaborator policy: %w", err) 279 + } 280 + if err := t.spindle.db.DeleteRepoCollaborator(actor, rkey); err != nil { 281 + l.Error("failed to delete collaborator row", "err", err) 282 + return fmt.Errorf("delete collaborator row: %w", err) 283 + } 284 + } 285 + return nil 286 + } 287 + 288 + func (t *Tap) bufferCollab(repoDid syntax.DID, evt *tapc.RecordEventData) { 289 + t.pendingMu.Lock() 290 + defer t.pendingMu.Unlock() 291 + list := t.pendingCollabs[repoDid] 292 + list = append(list, pendingCollabEvent{evt: evt, at: time.Now()}) 293 + if len(list) > maxPendingPerRepo { 294 + list = list[len(list)-maxPendingPerRepo:] 295 + } 296 + t.pendingCollabs[repoDid] = list 297 + } 298 + 299 + func (t *Tap) drainPendingCollabs(ctx context.Context, repoDid syntax.DID) { 300 + t.pendingMu.Lock() 301 + list := t.pendingCollabs[repoDid] 302 + delete(t.pendingCollabs, repoDid) 303 + t.pendingMu.Unlock() 304 + if len(list) == 0 { 305 + return 306 + } 307 + cutoff := time.Now().Add(-pendingCollabTTL) 308 + for _, p := range list { 309 + if p.at.Before(cutoff) { 310 + continue 311 + } 312 + if err := t.processCollaborator(ctx, p.evt); err != nil { 313 + t.logger.Warn("replaying buffered collaborator failed", "repo", repoDid, "rkey", p.evt.Rkey, "err", err) 314 + } 315 + } 316 + } 317 + 318 + func (t *Tap) purgePendingCollabsLoop(ctx context.Context) { 319 + ticker := time.NewTicker(pendingCollabTTL / 2) 320 + defer ticker.Stop() 321 + for { 322 + select { 323 + case <-ctx.Done(): 324 + return 325 + case <-ticker.C: 326 + t.purgeStalePendingCollabs() 327 + } 328 + } 329 + } 330 + 331 + func (t *Tap) purgeStalePendingCollabs() { 332 + cutoff := time.Now().Add(-pendingCollabTTL) 333 + t.pendingMu.Lock() 334 + defer t.pendingMu.Unlock() 335 + for did, list := range t.pendingCollabs { 336 + kept := list[:0] 337 + for _, p := range list { 338 + if !p.at.Before(cutoff) { 339 + kept = append(kept, p) 340 + } 341 + } 342 + if len(kept) == 0 { 343 + delete(t.pendingCollabs, did) 344 + } else { 345 + t.pendingCollabs[did] = kept 346 + } 347 + } 348 + }