spindle/secrets: add openbao manager #385

merged
opened by anirudh.fi targeting master from push-mzsvpkottnnt
+31 -13
go.mod
··· 1 1 module tangled.sh/tangled.sh/core 2 2 3 - go 1.24.0 4 - 5 - toolchain go1.24.3 3 + go 1.24.4 6 4 7 5 require ( 8 6 github.com/Blank-Xu/sql-adapter v1.1.1 7 + github.com/alecthomas/assert/v2 v2.11.0 9 8 github.com/alecthomas/chroma/v2 v2.15.0 10 9 github.com/avast/retry-go/v4 v4.6.1 11 10 github.com/bluekeyes/go-gitdiff v0.8.1 ··· 23 22 github.com/go-git/go-git/v5 v5.14.0 24 23 github.com/google/uuid v1.6.0 25 24 github.com/gorilla/sessions v1.4.0 26 - github.com/gorilla/websocket v1.5.3 25 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 27 26 github.com/hiddeco/sshsig v0.2.0 28 27 github.com/hpcloud/tail v1.0.0 29 28 github.com/ipfs/go-cid v0.5.0 30 29 github.com/lestrrat-go/jwx/v2 v2.1.6 31 30 github.com/mattn/go-sqlite3 v1.14.24 32 31 github.com/microcosm-cc/bluemonday v1.0.27 32 + github.com/openbao/openbao/api/v2 v2.3.0 33 33 github.com/posthog/posthog-go v1.5.5 34 - github.com/redis/go-redis/v9 v9.3.0 34 + github.com/redis/go-redis/v9 v9.7.3 35 35 github.com/resend/resend-go/v2 v2.15.0 36 36 github.com/sethvargo/go-envconfig v1.1.0 37 37 github.com/stretchr/testify v1.10.0 ··· 39 39 github.com/whyrusleeping/cbor-gen v0.3.1 40 40 github.com/yuin/goldmark v1.4.13 41 41 golang.org/x/crypto v0.40.0 42 - golang.org/x/net v0.41.0 42 + golang.org/x/net v0.42.0 43 + golang.org/x/sync v0.16.0 43 44 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 44 45 gopkg.in/yaml.v3 v3.0.1 45 46 tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1 ··· 48 49 require ( 49 50 dario.cat/mergo v1.0.1 // indirect 50 51 github.com/Microsoft/go-winio v0.6.2 // indirect 51 - github.com/ProtonMail/go-crypto v1.2.0 // indirect 52 + github.com/ProtonMail/go-crypto v1.3.0 // indirect 53 + github.com/alecthomas/repr v0.4.0 // indirect 52 54 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 53 55 github.com/aymerick/douceur v0.2.0 // indirect 54 56 github.com/beorn7/perks v1.0.1 // indirect 55 57 github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect 56 58 github.com/casbin/govaluate v1.3.0 // indirect 59 + github.com/cenkalti/backoff/v4 v4.3.0 // indirect 57 60 github.com/cespare/xxhash/v2 v2.3.0 // indirect 58 - github.com/cloudflare/circl v1.6.0 // indirect 61 + github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d // indirect 59 62 github.com/containerd/errdefs v1.0.0 // indirect 60 63 github.com/containerd/errdefs/pkg v0.3.0 // indirect 61 64 github.com/containerd/log v0.1.0 // indirect ··· 68 71 github.com/docker/go-units v0.5.0 // indirect 69 72 github.com/emirpasic/gods v1.18.1 // indirect 70 73 github.com/felixge/httpsnoop v1.0.4 // indirect 74 + github.com/fsnotify/fsnotify v1.6.0 // indirect 71 75 github.com/go-enry/go-oniguruma v1.2.1 // indirect 72 76 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 73 77 github.com/go-git/go-billy/v5 v5.6.2 // indirect 78 + github.com/go-jose/go-jose/v3 v3.0.4 // indirect 74 79 github.com/go-logr/logr v1.4.3 // indirect 75 80 github.com/go-logr/stdr v1.2.2 // indirect 76 81 github.com/go-redis/cache/v9 v9.0.0 // indirect 82 + github.com/go-test/deep v1.1.1 // indirect 77 83 github.com/goccy/go-json v0.10.5 // indirect 78 84 github.com/gogo/protobuf v1.3.2 // indirect 79 85 github.com/golang-jwt/jwt/v5 v5.2.3 // indirect 80 86 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 87 + github.com/golang/mock v1.6.0 // indirect 81 88 github.com/gorilla/css v1.0.1 // indirect 82 89 github.com/gorilla/securecookie v1.1.2 // indirect 90 + github.com/hashicorp/errwrap v1.1.0 // indirect 83 91 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 92 + github.com/hashicorp/go-multierror v1.1.1 // indirect 84 93 github.com/hashicorp/go-retryablehttp v0.7.8 // indirect 94 + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 95 + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 96 + github.com/hashicorp/go-sockaddr v1.0.7 // indirect 85 97 github.com/hashicorp/golang-lru v1.0.2 // indirect 86 98 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 99 + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 100 + github.com/hexops/gotextdiff v1.0.3 // indirect 87 101 github.com/ipfs/bbloom v0.0.4 // indirect 88 102 github.com/ipfs/boxo v0.33.0 // indirect 89 103 github.com/ipfs/go-block-format v0.2.2 // indirect ··· 105 119 github.com/lestrrat-go/option v1.0.1 // indirect 106 120 github.com/mattn/go-isatty v0.0.20 // indirect 107 121 github.com/minio/sha256-simd v1.0.1 // indirect 122 + github.com/mitchellh/mapstructure v1.5.0 // indirect 108 123 github.com/moby/docker-image-spec v1.3.1 // indirect 109 124 github.com/moby/sys/atomicwriter v0.1.0 // indirect 110 125 github.com/moby/term v0.5.2 // indirect ··· 116 131 github.com/multiformats/go-multihash v0.2.3 // indirect 117 132 github.com/multiformats/go-varint v0.0.7 // indirect 118 133 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 134 + github.com/onsi/gomega v1.37.0 // indirect 119 135 github.com/opencontainers/go-digest v1.0.0 // indirect 120 136 github.com/opencontainers/image-spec v1.1.1 // indirect 121 - github.com/opentracing/opentracing-go v1.2.0 // indirect 137 + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 122 138 github.com/pjbgf/sha1cd v0.3.2 // indirect 123 139 github.com/pkg/errors v0.9.1 // indirect 124 140 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ··· 127 143 github.com/prometheus/client_model v0.6.2 // indirect 128 144 github.com/prometheus/common v0.64.0 // indirect 129 145 github.com/prometheus/procfs v0.16.1 // indirect 146 + github.com/ryanuber/go-glob v1.0.0 // indirect 130 147 github.com/segmentio/asm v1.2.0 // indirect 131 148 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 132 149 github.com/spaolacci/murmur3 v1.1.0 // indirect ··· 138 155 go.opentelemetry.io/auto/sdk v1.1.0 // indirect 139 156 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect 140 157 go.opentelemetry.io/otel v1.37.0 // indirect 158 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 141 159 go.opentelemetry.io/otel/metric v1.37.0 // indirect 142 160 go.opentelemetry.io/otel/trace v1.37.0 // indirect 143 161 go.opentelemetry.io/proto/otlp v1.6.0 // indirect ··· 145 163 go.uber.org/multierr v1.11.0 // indirect 146 164 go.uber.org/zap v1.27.0 // indirect 147 165 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 148 - golang.org/x/sync v0.15.0 // indirect 149 166 golang.org/x/sys v0.34.0 // indirect 167 + golang.org/x/text v0.27.0 // indirect 150 168 golang.org/x/time v0.12.0 // indirect 151 - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect 152 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 153 - google.golang.org/grpc v1.72.1 // indirect 169 + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect 170 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect 171 + google.golang.org/grpc v1.73.0 // indirect 154 172 google.golang.org/protobuf v1.36.6 // indirect 155 173 gopkg.in/fsnotify.v1 v1.4.7 // indirect 156 174 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+80 -96
go.sum
··· 7 7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 8 8 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 9 9 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 10 - github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= 11 - github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 10 + github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= 11 + github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 12 12 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 13 13 github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 14 14 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= ··· 23 23 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 24 24 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 25 25 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 26 - github.com/bluesky-social/indigo v0.0.0-20250520232546-236dd575c91e h1:DVD+HxQsDCVJtAkjfIKZVaBNc3kayHaU+A2TJZkFdp4= 27 - github.com/bluesky-social/indigo v0.0.0-20250520232546-236dd575c91e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 28 26 github.com/bluesky-social/indigo v0.0.0-20250724221105-5827c8fb61bb h1:BqMNDZMfXwiRTJ6NvQotJ0qInn37JH5U8E+TF01CFHQ= 29 27 github.com/bluesky-social/indigo v0.0.0-20250724221105-5827c8fb61bb/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= 30 28 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= ··· 53 51 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 54 52 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 55 53 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 56 - github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= 57 - github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 54 + github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d h1:IiIprFGH6SqstblP0Y9NIo3eaUJGkI/YDOFVSL64Uq4= 55 + github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 58 56 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 59 57 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 60 58 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= ··· 93 91 github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 94 92 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 95 93 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 96 - github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 97 - github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 94 + github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 95 + github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 98 96 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 99 97 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 100 98 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 101 - github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 102 99 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 100 + github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 101 + github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 103 102 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 104 103 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 105 104 github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= ··· 116 115 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 117 116 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03 h1:LumE+tQdnYW24a9RoO08w64LHTzkNkdUqBD/0QPtlEY= 118 117 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM= 118 + github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 119 + github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 119 120 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 120 121 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 121 - github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 122 - github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 123 122 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 124 123 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 125 124 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= ··· 127 126 github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= 128 127 github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= 129 128 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 129 + github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 130 + github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 130 131 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 131 132 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 132 133 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 133 134 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 134 135 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 135 - github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 136 - github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 137 - github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 138 136 github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= 139 137 github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 140 138 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 141 139 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 142 - github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 143 140 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 141 + github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 142 + github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 144 143 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 145 144 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 146 145 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= ··· 173 172 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 174 173 github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 175 174 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 176 - github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 177 - github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 175 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 176 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 178 177 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 179 178 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 179 + github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 180 + github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 181 + github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 180 182 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 181 183 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 182 184 github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 183 185 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 184 - github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 185 - github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 186 + github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 187 + github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 186 188 github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= 187 189 github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= 190 + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= 191 + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= 192 + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 193 + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 194 + github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= 195 + github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= 188 196 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 189 197 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 190 198 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 191 199 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 200 + github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= 201 + github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 192 202 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 193 203 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 194 204 github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= ··· 198 208 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 199 209 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 200 210 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 201 - github.com/ipfs/boxo v0.30.0 h1:7afsoxPGGqfoH7Dum/wOTGUB9M5fb8HyKPMlLfBvIEQ= 202 - github.com/ipfs/boxo v0.30.0/go.mod h1:BPqgGGyHB9rZZcPSzah2Dc9C+5Or3U1aQe7EH1H7370= 203 211 github.com/ipfs/boxo v0.33.0 h1:9ow3chwkDzMj0Deq4AWRUEI7WnIIV7SZhPTzzG2mmfw= 204 212 github.com/ipfs/boxo v0.33.0/go.mod h1:3IPh7YFcCIcKp6o02mCHovrPntoT5Pctj/7j4syh/RM= 205 - github.com/ipfs/go-block-format v0.2.1 h1:96kW71XGNNa+mZw/MTzJrCpMhBWCrd9kBLoKm9Iip/Q= 206 - github.com/ipfs/go-block-format v0.2.1/go.mod h1:frtvXHMQhM6zn7HvEQu+Qz5wSTj+04oEH/I+NjDgEjk= 207 213 github.com/ipfs/go-block-format v0.2.2 h1:uecCTgRwDIXyZPgYspaLXoMiMmxQpSx2aq34eNc4YvQ= 208 214 github.com/ipfs/go-block-format v0.2.2/go.mod h1:vmuefuWU6b+9kIU0vZJgpiJt1yicQz9baHXE8qR+KB8= 209 215 github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= ··· 218 224 github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 219 225 github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 220 226 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 221 - github.com/ipfs/go-ipld-cbor v0.2.0 h1:VHIW3HVIjcMd8m4ZLZbrYpwjzqlVUfjLM7oK4T5/YF0= 222 - github.com/ipfs/go-ipld-cbor v0.2.0/go.mod h1:Cp8T7w1NKcu4AQJLqK0tWpd1nkgTxEVB5C6kVpLW6/0= 223 227 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 224 228 github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= 225 - github.com/ipfs/go-ipld-format v0.6.1 h1:lQLmBM/HHbrXvjIkrydRXkn+gc0DE5xO5fqelsCKYOQ= 226 - github.com/ipfs/go-ipld-format v0.6.1/go.mod h1:8TOH1Hj+LFyqM2PjSqI2/ZnyO0KlfhHbJLkbxFa61hs= 227 229 github.com/ipfs/go-ipld-format v0.6.2 h1:bPZQ+A05ol0b3lsJSl0bLvwbuQ+HQbSsdGTy4xtYUkU= 228 230 github.com/ipfs/go-ipld-format v0.6.2/go.mod h1:nni2xFdHKx5lxvXJ6brt/pndtGxKAE+FPR1rg4jTkyk= 229 231 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= ··· 233 235 github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8= 234 236 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 235 237 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 236 - github.com/ipfs/go-test v0.2.1 h1:/D/a8xZ2JzkYqcVcV/7HYlCnc7bv/pKHQiX5TdClkPE= 237 - github.com/ipfs/go-test v0.2.1/go.mod h1:dzu+KB9cmWjuJnXFDYJwC25T3j1GcN57byN+ixmK39M= 238 - github.com/ipfs/go-test v0.2.2 h1:1yjYyfbdt1w93lVzde6JZ2einh3DIV40at4rVoyEcE8= 239 238 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 240 239 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 241 240 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= ··· 247 246 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 248 247 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 249 248 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 250 - github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 251 - github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 252 249 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 253 250 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 254 251 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= ··· 259 256 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 260 257 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 261 258 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 262 - github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= 263 - github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 264 259 github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= 265 260 github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 266 261 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= ··· 273 268 github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= 274 269 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 275 270 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 276 - github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 277 - github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 278 - github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= 279 - github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= 280 - github.com/libp2p/go-libp2p v0.42.0 h1:A8foZk+ZEhZTv0Jb++7xUFlrFhBDv4j2Vh/uq4YX+KE= 281 - github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 282 - github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 271 + github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 272 + github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 283 273 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 284 274 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 285 275 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= ··· 288 278 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 289 279 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 290 280 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 281 + github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 282 + github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 291 283 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 292 284 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 293 285 github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= ··· 304 296 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 305 297 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 306 298 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 307 - github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo= 308 - github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= 309 - github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= 310 299 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 311 300 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 312 - github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 313 - github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 314 - github.com/multiformats/go-multicodec v0.9.2 h1:YrlXCuqxjqm3bXl+vBq5LKz5pz4mvAsugdqy78k0pXQ= 315 301 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 316 302 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 317 303 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= ··· 343 329 github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 344 330 github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 345 331 github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 346 - github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 347 - github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 332 + github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 333 + github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 334 + github.com/openbao/openbao/api/v2 v2.3.0 h1:61FO3ILtpKoxbD9kTWeGaCq8pz1sdt4dv2cmTXsiaAc= 335 + github.com/openbao/openbao/api/v2 v2.3.0/go.mod h1:T47WKHb7DqHa3Ms3xicQtl5EiPE+U8diKjb9888okWs= 348 336 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 349 337 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 350 338 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 351 339 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 352 - github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 353 340 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 341 + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= 342 + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= 354 343 github.com/oppiliappan/chroma/v2 v2.19.0 h1:PN7/pb+6JRKCva30NPTtRJMlrOyzgpPpIroNzy4ekHU= 355 344 github.com/oppiliappan/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 356 345 github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo= ··· 371 360 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 372 361 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 373 362 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 374 - github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 375 - github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 376 363 github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 377 364 github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 378 365 github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 379 366 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 380 367 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 381 - github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= 382 - github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 368 + github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 369 + github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 383 370 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 384 371 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 385 372 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= ··· 387 374 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 388 375 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 389 376 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 377 + github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 378 + github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 390 379 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 391 380 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 392 381 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= ··· 431 420 github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 432 421 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 433 422 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 423 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 434 424 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 435 425 github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 436 426 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= ··· 440 430 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 441 431 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 442 432 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 443 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= 444 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= 445 433 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= 446 434 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= 447 - go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= 448 - go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= 449 435 go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 450 436 go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 451 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= 452 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= 437 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 438 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 453 439 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= 454 440 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= 455 - go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= 456 - go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= 457 441 go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 458 442 go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 459 - go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= 460 - go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= 461 443 go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 462 - go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= 463 - go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= 444 + go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 464 445 go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 465 - go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= 466 - go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= 446 + go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 467 447 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 468 448 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 469 449 go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= ··· 488 468 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 489 469 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 490 470 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 491 - golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 492 - golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 471 + golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 493 472 golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 494 473 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 495 - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 496 - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 497 474 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 498 475 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 499 476 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 500 477 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 501 478 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 502 479 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 480 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 503 481 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 504 482 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 505 483 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 506 484 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 485 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 507 486 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 508 487 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 509 488 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= ··· 512 491 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 513 492 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 514 493 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 494 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 515 495 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 516 496 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 517 497 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= ··· 521 501 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 522 502 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 523 503 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 524 - golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 525 - golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 526 - golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 527 - golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 504 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 505 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 506 + golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 507 + golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 528 508 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 529 509 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 530 510 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 532 512 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 533 513 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 534 514 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 535 - golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 536 - golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 537 - golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 538 - golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 515 + golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 516 + golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 539 517 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 540 518 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 541 519 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 547 525 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 548 526 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 549 527 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 528 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 550 529 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 530 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 551 531 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 552 532 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 553 533 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 555 535 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 556 536 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 557 537 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 538 + golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 558 539 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 559 540 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 560 541 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 561 542 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 543 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 562 544 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 563 - golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 564 - golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 545 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 546 + golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 565 547 golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 566 548 golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 567 549 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= ··· 570 552 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 571 553 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 572 554 golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 573 - golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 574 - golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 555 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 556 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 557 + golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 575 558 golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 559 + golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 576 560 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 577 561 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 578 562 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 580 564 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 581 565 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 582 566 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 583 - golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 584 - golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 567 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 568 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 569 + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 585 570 golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 586 - golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 587 - golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 571 + golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 588 572 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 589 573 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 590 574 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 598 582 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 599 583 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 600 584 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 585 + golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 601 586 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 602 587 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 603 588 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 604 589 golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 590 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 605 591 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 606 592 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 607 593 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 608 594 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 609 595 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 610 596 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 611 - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= 612 - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= 613 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= 614 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 615 - google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 616 - google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 597 + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= 598 + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= 599 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= 600 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 601 + google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 602 + google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 617 603 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 618 604 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 619 605 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 650 636 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 651 637 lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 652 638 lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= 653 - tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250526154904-3906c5336421 h1:ZQNKE1HKWjfQBizxDb2XLhrJcgZqE0MLFIU1iggaF90= 654 - tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250526154904-3906c5336421/go.mod h1:Wad0H70uyyY4qZryU/1Ic+QZyw41YxN5QfzNAEBaXkQ= 655 639 tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1 h1:z1os1aRIqeo5e8d0Tx7hk+LH8OdZZeIOY0zw9VB/ZoU= 656 640 tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1/go.mod h1:+oQi9S6IIDll0nxLZVhuzOPX8WKLCYEnE6M5kUKupDg= 657 641 tangled.sh/oppi.li/go-gitdiff v0.8.2 h1:pASJJNWaFn6EmEIUNNjHZQ3stRu6BqTO2YyjKvTcxIc=
+9 -1
spindle/config/config.go
··· 23 23 } 24 24 25 25 type Secrets struct { 26 - Provider string `env:"PROVIDER, default=sqlite"` 26 + Provider string `env:"PROVIDER, default=sqlite"` 27 + OpenBao OpenBaoConfig `env:",prefix=OPENBAO_"` 28 + } 29 + 30 + type OpenBaoConfig struct { 31 + Addr string `env:"ADDR"` 32 + RoleID string `env:"ROLE_ID"` 33 + SecretID string `env:"SECRET_ID"` 34 + Mount string `env:"MOUNT, default=spindle"` 27 35 } 28 36 29 37 type Pipelines struct {
+1 -1
spindle/engine/engine.go
··· 74 74 // extract secrets 75 75 var allSecrets []secrets.UnlockedSecret 76 76 if didSlashRepo, err := securejoin.SecureJoin(pipeline.RepoOwner, pipeline.RepoName); err == nil { 77 - if res, err := e.vault.GetSecretsUnlocked(secrets.DidSlashRepo(didSlashRepo)); err == nil { 77 + if res, err := e.vault.GetSecretsUnlocked(ctx, secrets.DidSlashRepo(didSlashRepo)); err == nil { 78 78 allSecrets = res 79 79 } 80 80 }
+11 -4
spindle/secrets/manager.go
··· 1 1 package secrets 2 2 3 3 import ( 4 + "context" 4 5 "errors" 5 6 "regexp" 6 7 "time" ··· 26 27 type UnlockedSecret = Secret[string] 27 28 28 29 type Manager interface { 29 - AddSecret(secret UnlockedSecret) error 30 - RemoveSecret(secret Secret[any]) error 31 - GetSecretsLocked(repo DidSlashRepo) ([]LockedSecret, error) 32 - GetSecretsUnlocked(repo DidSlashRepo) ([]UnlockedSecret, error) 30 + AddSecret(ctx context.Context, secret UnlockedSecret) error 31 + RemoveSecret(ctx context.Context, secret Secret[any]) error 32 + GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) 33 + GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) 34 + } 35 + 36 + // stopper interface for managers that need cleanup 37 + type Stopper interface { 38 + Stop() 33 39 } 34 40 35 41 var ErrKeyAlreadyPresent = errors.New("key already present") ··· 40 46 var ( 41 47 _ = []Manager{ 42 48 &SqliteManager{}, 49 + &OpenBaoManager{}, 43 50 } 44 51 ) 45 52
+407
spindle/secrets/openbao.go
··· 1 + package secrets 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "path" 8 + "strings" 9 + "sync" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + vault "github.com/openbao/openbao/api/v2" 14 + ) 15 + 16 + type OpenBaoManager struct { 17 + client *vault.Client 18 + mountPath string 19 + roleID string 20 + secretID string 21 + stopCh chan struct{} 22 + tokenMu sync.RWMutex 23 + logger *slog.Logger 24 + } 25 + 26 + type OpenBaoManagerOpt func(*OpenBaoManager) 27 + 28 + func WithMountPath(mountPath string) OpenBaoManagerOpt { 29 + return func(v *OpenBaoManager) { 30 + v.mountPath = mountPath 31 + } 32 + } 33 + 34 + func NewOpenBaoManager(address, roleID, secretID string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) { 35 + if address == "" { 36 + return nil, fmt.Errorf("address cannot be empty") 37 + } 38 + if roleID == "" { 39 + return nil, fmt.Errorf("role_id cannot be empty") 40 + } 41 + if secretID == "" { 42 + return nil, fmt.Errorf("secret_id cannot be empty") 43 + } 44 + 45 + config := vault.DefaultConfig() 46 + config.Address = address 47 + 48 + client, err := vault.NewClient(config) 49 + if err != nil { 50 + return nil, fmt.Errorf("failed to create openbao client: %w", err) 51 + } 52 + 53 + // Authenticate using AppRole 54 + err = authenticateAppRole(client, roleID, secretID) 55 + if err != nil { 56 + return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err) 57 + } 58 + 59 + manager := &OpenBaoManager{ 60 + client: client, 61 + mountPath: "spindle", // default KV v2 mount path 62 + roleID: roleID, 63 + secretID: secretID, 64 + stopCh: make(chan struct{}), 65 + logger: logger, 66 + } 67 + 68 + for _, opt := range opts { 69 + opt(manager) 70 + } 71 + 72 + go manager.tokenRenewalLoop() 73 + 74 + return manager, nil 75 + } 76 + 77 + // authenticateAppRole authenticates the client using AppRole method 78 + func authenticateAppRole(client *vault.Client, roleID, secretID string) error { 79 + authData := map[string]interface{}{ 80 + "role_id": roleID, 81 + "secret_id": secretID, 82 + } 83 + 84 + resp, err := client.Logical().Write("auth/approle/login", authData) 85 + if err != nil { 86 + return fmt.Errorf("failed to login with AppRole: %w", err) 87 + } 88 + 89 + if resp == nil || resp.Auth == nil { 90 + return fmt.Errorf("no auth info returned from AppRole login") 91 + } 92 + 93 + client.SetToken(resp.Auth.ClientToken) 94 + return nil 95 + } 96 + 97 + // stop stops the token renewal goroutine 98 + func (v *OpenBaoManager) Stop() { 99 + close(v.stopCh) 100 + } 101 + 102 + // tokenRenewalLoop runs in a background goroutine to automatically renew or re-authenticate tokens 103 + func (v *OpenBaoManager) tokenRenewalLoop() { 104 + ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds 105 + defer ticker.Stop() 106 + 107 + for { 108 + select { 109 + case <-v.stopCh: 110 + return 111 + case <-ticker.C: 112 + ctx := context.Background() 113 + if err := v.ensureValidToken(ctx); err != nil { 114 + v.logger.Error("openbao token renewal failed", "error", err) 115 + } 116 + } 117 + } 118 + } 119 + 120 + // ensureValidToken checks if the current token is valid and renews or re-authenticates if needed 121 + func (v *OpenBaoManager) ensureValidToken(ctx context.Context) error { 122 + v.tokenMu.Lock() 123 + defer v.tokenMu.Unlock() 124 + 125 + // check current token info 126 + tokenInfo, err := v.client.Auth().Token().LookupSelf() 127 + if err != nil { 128 + // token is invalid, need to re-authenticate 129 + v.logger.Warn("token lookup failed, re-authenticating", "error", err) 130 + return v.reAuthenticate() 131 + } 132 + 133 + if tokenInfo == nil || tokenInfo.Data == nil { 134 + return v.reAuthenticate() 135 + } 136 + 137 + // check TTL 138 + ttlRaw, ok := tokenInfo.Data["ttl"] 139 + if !ok { 140 + return v.reAuthenticate() 141 + } 142 + 143 + var ttl int64 144 + switch t := ttlRaw.(type) { 145 + case int64: 146 + ttl = t 147 + case float64: 148 + ttl = int64(t) 149 + case int: 150 + ttl = int64(t) 151 + default: 152 + return v.reAuthenticate() 153 + } 154 + 155 + // if TTL is less than 5 minutes, try to renew 156 + if ttl < 300 { 157 + v.logger.Info("token ttl low, attempting renewal", "ttl_seconds", ttl) 158 + 159 + renewResp, err := v.client.Auth().Token().RenewSelf(3600) // 1h 160 + if err != nil { 161 + v.logger.Warn("token renewal failed, re-authenticating", "error", err) 162 + return v.reAuthenticate() 163 + } 164 + 165 + if renewResp == nil || renewResp.Auth == nil { 166 + v.logger.Warn("token renewal returned no auth info, re-authenticating") 167 + return v.reAuthenticate() 168 + } 169 + 170 + v.logger.Info("token renewed successfully", "new_ttl_seconds", renewResp.Auth.LeaseDuration) 171 + } 172 + 173 + return nil 174 + } 175 + 176 + // reAuthenticate performs a fresh authentication using AppRole 177 + func (v *OpenBaoManager) reAuthenticate() error { 178 + v.logger.Info("re-authenticating with approle") 179 + 180 + err := authenticateAppRole(v.client, v.roleID, v.secretID) 181 + if err != nil { 182 + return fmt.Errorf("re-authentication failed: %w", err) 183 + } 184 + 185 + v.logger.Info("re-authentication successful") 186 + return nil 187 + } 188 + 189 + func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error { 190 + v.tokenMu.RLock() 191 + defer v.tokenMu.RUnlock() 192 + if err := ValidateKey(secret.Key); err != nil { 193 + return err 194 + } 195 + 196 + secretPath := v.buildSecretPath(secret.Repo, secret.Key) 197 + 198 + fmt.Println(v.mountPath, secretPath) 199 + 200 + existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 201 + if err == nil && existing != nil { 202 + return ErrKeyAlreadyPresent 203 + } 204 + 205 + secretData := map[string]interface{}{ 206 + "value": secret.Value, 207 + "repo": string(secret.Repo), 208 + "key": secret.Key, 209 + "created_at": secret.CreatedAt.Format(time.RFC3339), 210 + "created_by": secret.CreatedBy.String(), 211 + } 212 + 213 + _, err = v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData) 214 + if err != nil { 215 + return fmt.Errorf("failed to store secret in openbao: %w", err) 216 + } 217 + 218 + return nil 219 + } 220 + 221 + func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error { 222 + v.tokenMu.RLock() 223 + defer v.tokenMu.RUnlock() 224 + secretPath := v.buildSecretPath(secret.Repo, secret.Key) 225 + 226 + existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 227 + if err != nil || existing == nil { 228 + return ErrKeyNotFound 229 + } 230 + 231 + err = v.client.KVv2(v.mountPath).Delete(ctx, secretPath) 232 + if err != nil { 233 + return fmt.Errorf("failed to delete secret from openbao: %w", err) 234 + } 235 + 236 + return nil 237 + } 238 + 239 + func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 240 + v.tokenMu.RLock() 241 + defer v.tokenMu.RUnlock() 242 + repoPath := v.buildRepoPath(repo) 243 + 244 + secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) 245 + if err != nil { 246 + if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { 247 + return []LockedSecret{}, nil 248 + } 249 + return nil, fmt.Errorf("failed to list secrets: %w", err) 250 + } 251 + 252 + if secretsList == nil || secretsList.Data == nil { 253 + return []LockedSecret{}, nil 254 + } 255 + 256 + keys, ok := secretsList.Data["keys"].([]interface{}) 257 + if !ok { 258 + return []LockedSecret{}, nil 259 + } 260 + 261 + var secrets []LockedSecret 262 + 263 + for _, keyInterface := range keys { 264 + key, ok := keyInterface.(string) 265 + if !ok { 266 + continue 267 + } 268 + 269 + secretPath := path.Join(repoPath, key) 270 + secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 271 + if err != nil { 272 + continue // Skip secrets we can't read 273 + } 274 + 275 + if secretData == nil || secretData.Data == nil { 276 + continue 277 + } 278 + 279 + data := secretData.Data 280 + 281 + createdAtStr, ok := data["created_at"].(string) 282 + if !ok { 283 + createdAtStr = time.Now().Format(time.RFC3339) 284 + } 285 + 286 + createdAt, err := time.Parse(time.RFC3339, createdAtStr) 287 + if err != nil { 288 + createdAt = time.Now() 289 + } 290 + 291 + createdByStr, ok := data["created_by"].(string) 292 + if !ok { 293 + createdByStr = "" 294 + } 295 + 296 + keyStr, ok := data["key"].(string) 297 + if !ok { 298 + keyStr = key 299 + } 300 + 301 + secret := LockedSecret{ 302 + Key: keyStr, 303 + Repo: repo, 304 + CreatedAt: createdAt, 305 + CreatedBy: syntax.DID(createdByStr), 306 + } 307 + 308 + secrets = append(secrets, secret) 309 + } 310 + 311 + return secrets, nil 312 + } 313 + 314 + func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 315 + v.tokenMu.RLock() 316 + defer v.tokenMu.RUnlock() 317 + repoPath := v.buildRepoPath(repo) 318 + 319 + secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) 320 + if err != nil { 321 + if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { 322 + return []UnlockedSecret{}, nil 323 + } 324 + return nil, fmt.Errorf("failed to list secrets: %w", err) 325 + } 326 + 327 + if secretsList == nil || secretsList.Data == nil { 328 + return []UnlockedSecret{}, nil 329 + } 330 + 331 + keys, ok := secretsList.Data["keys"].([]interface{}) 332 + if !ok { 333 + return []UnlockedSecret{}, nil 334 + } 335 + 336 + var secrets []UnlockedSecret 337 + 338 + for _, keyInterface := range keys { 339 + key, ok := keyInterface.(string) 340 + if !ok { 341 + continue 342 + } 343 + 344 + secretPath := path.Join(repoPath, key) 345 + secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 346 + if err != nil { 347 + continue 348 + } 349 + 350 + if secretData == nil || secretData.Data == nil { 351 + continue 352 + } 353 + 354 + data := secretData.Data 355 + 356 + valueStr, ok := data["value"].(string) 357 + if !ok { 358 + continue // skip secrets without values 359 + } 360 + 361 + createdAtStr, ok := data["created_at"].(string) 362 + if !ok { 363 + createdAtStr = time.Now().Format(time.RFC3339) 364 + } 365 + 366 + createdAt, err := time.Parse(time.RFC3339, createdAtStr) 367 + if err != nil { 368 + createdAt = time.Now() 369 + } 370 + 371 + createdByStr, ok := data["created_by"].(string) 372 + if !ok { 373 + createdByStr = "" 374 + } 375 + 376 + keyStr, ok := data["key"].(string) 377 + if !ok { 378 + keyStr = key 379 + } 380 + 381 + secret := UnlockedSecret{ 382 + Key: keyStr, 383 + Value: valueStr, 384 + Repo: repo, 385 + CreatedAt: createdAt, 386 + CreatedBy: syntax.DID(createdByStr), 387 + } 388 + 389 + secrets = append(secrets, secret) 390 + } 391 + 392 + return secrets, nil 393 + } 394 + 395 + // buildRepoPath creates an OpenBao path for a repository 396 + func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string { 397 + // convert DidSlashRepo to a safe path by replacing special characters 398 + repoPath := strings.ReplaceAll(string(repo), "/", "_") 399 + repoPath = strings.ReplaceAll(repoPath, ":", "_") 400 + repoPath = strings.ReplaceAll(repoPath, ".", "_") 401 + return fmt.Sprintf("repos/%s", repoPath) 402 + } 403 + 404 + // buildSecretPath creates an OpenBao path for a specific secret 405 + func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string { 406 + return path.Join(v.buildRepoPath(repo), key) 407 + }
+630
spindle/secrets/openbao_test.go
··· 1 + package secrets 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "os" 7 + "testing" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/stretchr/testify/assert" 12 + ) 13 + 14 + // MockOpenBaoManager is a mock implementation of Manager interface for testing 15 + type MockOpenBaoManager struct { 16 + secrets map[string]UnlockedSecret // key: repo_key format 17 + shouldError bool 18 + errorToReturn error 19 + stopped bool 20 + } 21 + 22 + func NewMockOpenBaoManager() *MockOpenBaoManager { 23 + return &MockOpenBaoManager{secrets: make(map[string]UnlockedSecret)} 24 + } 25 + 26 + func (m *MockOpenBaoManager) SetError(err error) { 27 + m.shouldError = true 28 + m.errorToReturn = err 29 + } 30 + 31 + func (m *MockOpenBaoManager) ClearError() { 32 + m.shouldError = false 33 + m.errorToReturn = nil 34 + } 35 + 36 + func (m *MockOpenBaoManager) Stop() { 37 + m.stopped = true 38 + } 39 + 40 + func (m *MockOpenBaoManager) IsStopped() bool { 41 + return m.stopped 42 + } 43 + 44 + func (m *MockOpenBaoManager) buildKey(repo DidSlashRepo, key string) string { 45 + return string(repo) + "_" + key 46 + } 47 + 48 + func (m *MockOpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error { 49 + if m.shouldError { 50 + return m.errorToReturn 51 + } 52 + 53 + key := m.buildKey(secret.Repo, secret.Key) 54 + if _, exists := m.secrets[key]; exists { 55 + return ErrKeyAlreadyPresent 56 + } 57 + 58 + m.secrets[key] = secret 59 + return nil 60 + } 61 + 62 + func (m *MockOpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error { 63 + if m.shouldError { 64 + return m.errorToReturn 65 + } 66 + 67 + key := m.buildKey(secret.Repo, secret.Key) 68 + if _, exists := m.secrets[key]; !exists { 69 + return ErrKeyNotFound 70 + } 71 + 72 + delete(m.secrets, key) 73 + return nil 74 + } 75 + 76 + func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 77 + if m.shouldError { 78 + return nil, m.errorToReturn 79 + } 80 + 81 + var result []LockedSecret 82 + for _, secret := range m.secrets { 83 + if secret.Repo == repo { 84 + result = append(result, LockedSecret{ 85 + Key: secret.Key, 86 + Repo: secret.Repo, 87 + CreatedAt: secret.CreatedAt, 88 + CreatedBy: secret.CreatedBy, 89 + }) 90 + } 91 + } 92 + 93 + return result, nil 94 + } 95 + 96 + func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 97 + if m.shouldError { 98 + return nil, m.errorToReturn 99 + } 100 + 101 + var result []UnlockedSecret 102 + for _, secret := range m.secrets { 103 + if secret.Repo == repo { 104 + result = append(result, secret) 105 + } 106 + } 107 + 108 + return result, nil 109 + } 110 + 111 + func createTestSecretForOpenBao(repo, key, value, createdBy string) UnlockedSecret { 112 + return UnlockedSecret{ 113 + Key: key, 114 + Value: value, 115 + Repo: DidSlashRepo(repo), 116 + CreatedAt: time.Now(), 117 + CreatedBy: syntax.DID(createdBy), 118 + } 119 + } 120 + 121 + func TestOpenBaoManagerInterface(t *testing.T) { 122 + var _ Manager = (*OpenBaoManager)(nil) 123 + } 124 + 125 + func TestNewOpenBaoManager(t *testing.T) { 126 + tests := []struct { 127 + name string 128 + address string 129 + roleID string 130 + secretID string 131 + opts []OpenBaoManagerOpt 132 + expectError bool 133 + errorContains string 134 + }{ 135 + { 136 + name: "empty address", 137 + address: "", 138 + roleID: "test-role-id", 139 + secretID: "test-secret-id", 140 + opts: nil, 141 + expectError: true, 142 + errorContains: "address cannot be empty", 143 + }, 144 + { 145 + name: "empty role_id", 146 + address: "http://localhost:8200", 147 + roleID: "", 148 + secretID: "test-secret-id", 149 + opts: nil, 150 + expectError: true, 151 + errorContains: "role_id cannot be empty", 152 + }, 153 + { 154 + name: "empty secret_id", 155 + address: "http://localhost:8200", 156 + roleID: "test-role-id", 157 + secretID: "", 158 + opts: nil, 159 + expectError: true, 160 + errorContains: "secret_id cannot be empty", 161 + }, 162 + } 163 + 164 + for _, tt := range tests { 165 + t.Run(tt.name, func(t *testing.T) { 166 + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) 167 + manager, err := NewOpenBaoManager(tt.address, tt.roleID, tt.secretID, logger, tt.opts...) 168 + 169 + if tt.expectError { 170 + assert.Error(t, err) 171 + assert.Nil(t, manager) 172 + assert.Contains(t, err.Error(), tt.errorContains) 173 + } else { 174 + // For valid configurations, we expect an error during authentication 175 + // since we're not connecting to a real OpenBao server 176 + assert.Error(t, err) 177 + assert.Nil(t, manager) 178 + } 179 + }) 180 + } 181 + } 182 + 183 + func TestOpenBaoManager_PathBuilding(t *testing.T) { 184 + manager := &OpenBaoManager{mountPath: "secret"} 185 + 186 + tests := []struct { 187 + name string 188 + repo DidSlashRepo 189 + key string 190 + expected string 191 + }{ 192 + { 193 + name: "simple repo path", 194 + repo: DidSlashRepo("did:plc:foo/repo"), 195 + key: "api_key", 196 + expected: "repos/did_plc_foo_repo/api_key", 197 + }, 198 + { 199 + name: "complex repo path with dots", 200 + repo: DidSlashRepo("did:web:example.com/my-repo"), 201 + key: "secret_key", 202 + expected: "repos/did_web_example_com_my-repo/secret_key", 203 + }, 204 + } 205 + 206 + for _, tt := range tests { 207 + t.Run(tt.name, func(t *testing.T) { 208 + result := manager.buildSecretPath(tt.repo, tt.key) 209 + assert.Equal(t, tt.expected, result) 210 + }) 211 + } 212 + } 213 + 214 + func TestOpenBaoManager_buildRepoPath(t *testing.T) { 215 + manager := &OpenBaoManager{mountPath: "test"} 216 + 217 + tests := []struct { 218 + name string 219 + repo DidSlashRepo 220 + expected string 221 + }{ 222 + { 223 + name: "simple repo", 224 + repo: "did:plc:test/myrepo", 225 + expected: "repos/did_plc_test_myrepo", 226 + }, 227 + { 228 + name: "repo with dots", 229 + repo: "did:plc:example.com/my.repo", 230 + expected: "repos/did_plc_example_com_my_repo", 231 + }, 232 + { 233 + name: "complex repo", 234 + repo: "did:web:example.com:8080/path/to/repo", 235 + expected: "repos/did_web_example_com_8080_path_to_repo", 236 + }, 237 + } 238 + 239 + for _, tt := range tests { 240 + t.Run(tt.name, func(t *testing.T) { 241 + result := manager.buildRepoPath(tt.repo) 242 + assert.Equal(t, tt.expected, result) 243 + }) 244 + } 245 + } 246 + 247 + func TestWithMountPath(t *testing.T) { 248 + manager := &OpenBaoManager{mountPath: "default"} 249 + 250 + opt := WithMountPath("custom-mount") 251 + opt(manager) 252 + 253 + assert.Equal(t, "custom-mount", manager.mountPath) 254 + } 255 + 256 + func TestOpenBaoManager_Stop(t *testing.T) { 257 + // Create a manager with minimal setup 258 + manager := &OpenBaoManager{ 259 + mountPath: "test", 260 + stopCh: make(chan struct{}), 261 + } 262 + 263 + // Verify the manager implements Stopper interface 264 + var stopper Stopper = manager 265 + assert.NotNil(t, stopper) 266 + 267 + // Call Stop and verify it doesn't panic 268 + assert.NotPanics(t, func() { 269 + manager.Stop() 270 + }) 271 + 272 + // Verify the channel was closed 273 + select { 274 + case <-manager.stopCh: 275 + // Channel was closed as expected 276 + default: 277 + t.Error("Expected stop channel to be closed after Stop()") 278 + } 279 + } 280 + 281 + func TestOpenBaoManager_StopperInterface(t *testing.T) { 282 + manager := &OpenBaoManager{} 283 + 284 + // Verify that OpenBaoManager implements the Stopper interface 285 + _, ok := interface{}(manager).(Stopper) 286 + assert.True(t, ok, "OpenBaoManager should implement Stopper interface") 287 + } 288 + 289 + // Test MockOpenBaoManager interface compliance 290 + func TestMockOpenBaoManagerInterface(t *testing.T) { 291 + var _ Manager = (*MockOpenBaoManager)(nil) 292 + var _ Stopper = (*MockOpenBaoManager)(nil) 293 + } 294 + 295 + func TestMockOpenBaoManager_AddSecret(t *testing.T) { 296 + tests := []struct { 297 + name string 298 + secrets []UnlockedSecret 299 + expectError bool 300 + }{ 301 + { 302 + name: "add single secret", 303 + secrets: []UnlockedSecret{ 304 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 305 + }, 306 + expectError: false, 307 + }, 308 + { 309 + name: "add multiple secrets", 310 + secrets: []UnlockedSecret{ 311 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 312 + createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 313 + }, 314 + expectError: false, 315 + }, 316 + { 317 + name: "add duplicate secret", 318 + secrets: []UnlockedSecret{ 319 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 320 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "newsecret", "did:plc:creator"), 321 + }, 322 + expectError: true, 323 + }, 324 + } 325 + 326 + for _, tt := range tests { 327 + t.Run(tt.name, func(t *testing.T) { 328 + mock := NewMockOpenBaoManager() 329 + ctx := context.Background() 330 + var err error 331 + 332 + for i, secret := range tt.secrets { 333 + err = mock.AddSecret(ctx, secret) 334 + if tt.expectError && i == 1 { // Second secret should fail for duplicate test 335 + assert.Equal(t, ErrKeyAlreadyPresent, err) 336 + return 337 + } 338 + if !tt.expectError { 339 + assert.NoError(t, err) 340 + } 341 + } 342 + 343 + if !tt.expectError { 344 + assert.NoError(t, err) 345 + } 346 + }) 347 + } 348 + } 349 + 350 + func TestMockOpenBaoManager_RemoveSecret(t *testing.T) { 351 + tests := []struct { 352 + name string 353 + setupSecrets []UnlockedSecret 354 + removeSecret Secret[any] 355 + expectError bool 356 + }{ 357 + { 358 + name: "remove existing secret", 359 + setupSecrets: []UnlockedSecret{ 360 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 361 + }, 362 + removeSecret: Secret[any]{ 363 + Key: "API_KEY", 364 + Repo: DidSlashRepo("did:plc:test/repo1"), 365 + }, 366 + expectError: false, 367 + }, 368 + { 369 + name: "remove non-existent secret", 370 + setupSecrets: []UnlockedSecret{}, 371 + removeSecret: Secret[any]{ 372 + Key: "API_KEY", 373 + Repo: DidSlashRepo("did:plc:test/repo1"), 374 + }, 375 + expectError: true, 376 + }, 377 + } 378 + 379 + for _, tt := range tests { 380 + t.Run(tt.name, func(t *testing.T) { 381 + mock := NewMockOpenBaoManager() 382 + ctx := context.Background() 383 + 384 + // Setup secrets 385 + for _, secret := range tt.setupSecrets { 386 + err := mock.AddSecret(ctx, secret) 387 + assert.NoError(t, err) 388 + } 389 + 390 + // Remove secret 391 + err := mock.RemoveSecret(ctx, tt.removeSecret) 392 + 393 + if tt.expectError { 394 + assert.Equal(t, ErrKeyNotFound, err) 395 + } else { 396 + assert.NoError(t, err) 397 + } 398 + }) 399 + } 400 + } 401 + 402 + func TestMockOpenBaoManager_GetSecretsLocked(t *testing.T) { 403 + tests := []struct { 404 + name string 405 + setupSecrets []UnlockedSecret 406 + queryRepo DidSlashRepo 407 + expectedCount int 408 + expectedKeys []string 409 + expectError bool 410 + }{ 411 + { 412 + name: "get secrets from repo with secrets", 413 + setupSecrets: []UnlockedSecret{ 414 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 415 + createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 416 + createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 417 + }, 418 + queryRepo: DidSlashRepo("did:plc:test/repo1"), 419 + expectedCount: 2, 420 + expectedKeys: []string{"API_KEY", "DB_PASSWORD"}, 421 + expectError: false, 422 + }, 423 + { 424 + name: "get secrets from empty repo", 425 + setupSecrets: []UnlockedSecret{}, 426 + queryRepo: DidSlashRepo("did:plc:test/empty"), 427 + expectedCount: 0, 428 + expectedKeys: []string{}, 429 + expectError: false, 430 + }, 431 + } 432 + 433 + for _, tt := range tests { 434 + t.Run(tt.name, func(t *testing.T) { 435 + mock := NewMockOpenBaoManager() 436 + ctx := context.Background() 437 + 438 + // Setup 439 + for _, secret := range tt.setupSecrets { 440 + err := mock.AddSecret(ctx, secret) 441 + assert.NoError(t, err) 442 + } 443 + 444 + // Test 445 + secrets, err := mock.GetSecretsLocked(ctx, tt.queryRepo) 446 + 447 + if tt.expectError { 448 + assert.Error(t, err) 449 + } else { 450 + assert.NoError(t, err) 451 + assert.Len(t, secrets, tt.expectedCount) 452 + 453 + // Check keys 454 + actualKeys := make([]string, len(secrets)) 455 + for i, secret := range secrets { 456 + actualKeys[i] = secret.Key 457 + } 458 + 459 + for _, expectedKey := range tt.expectedKeys { 460 + assert.Contains(t, actualKeys, expectedKey) 461 + } 462 + } 463 + }) 464 + } 465 + } 466 + 467 + func TestMockOpenBaoManager_GetSecretsUnlocked(t *testing.T) { 468 + tests := []struct { 469 + name string 470 + setupSecrets []UnlockedSecret 471 + queryRepo DidSlashRepo 472 + expectedCount int 473 + expectedSecrets map[string]string // key -> value 474 + expectError bool 475 + }{ 476 + { 477 + name: "get unlocked secrets from repo", 478 + setupSecrets: []UnlockedSecret{ 479 + createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"), 480 + createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 481 + createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 482 + }, 483 + queryRepo: DidSlashRepo("did:plc:test/repo1"), 484 + expectedCount: 2, 485 + expectedSecrets: map[string]string{ 486 + "API_KEY": "secret123", 487 + "DB_PASSWORD": "dbpass456", 488 + }, 489 + expectError: false, 490 + }, 491 + { 492 + name: "get secrets from empty repo", 493 + setupSecrets: []UnlockedSecret{}, 494 + queryRepo: DidSlashRepo("did:plc:test/empty"), 495 + expectedCount: 0, 496 + expectedSecrets: map[string]string{}, 497 + expectError: false, 498 + }, 499 + } 500 + 501 + for _, tt := range tests { 502 + t.Run(tt.name, func(t *testing.T) { 503 + mock := NewMockOpenBaoManager() 504 + ctx := context.Background() 505 + 506 + // Setup 507 + for _, secret := range tt.setupSecrets { 508 + err := mock.AddSecret(ctx, secret) 509 + assert.NoError(t, err) 510 + } 511 + 512 + // Test 513 + secrets, err := mock.GetSecretsUnlocked(ctx, tt.queryRepo) 514 + 515 + if tt.expectError { 516 + assert.Error(t, err) 517 + } else { 518 + assert.NoError(t, err) 519 + assert.Len(t, secrets, tt.expectedCount) 520 + 521 + // Check key-value pairs 522 + actualSecrets := make(map[string]string) 523 + for _, secret := range secrets { 524 + actualSecrets[secret.Key] = secret.Value 525 + } 526 + 527 + for expectedKey, expectedValue := range tt.expectedSecrets { 528 + actualValue, exists := actualSecrets[expectedKey] 529 + assert.True(t, exists, "Expected key %s not found", expectedKey) 530 + assert.Equal(t, expectedValue, actualValue) 531 + } 532 + } 533 + }) 534 + } 535 + } 536 + 537 + func TestMockOpenBaoManager_ErrorHandling(t *testing.T) { 538 + mock := NewMockOpenBaoManager() 539 + ctx := context.Background() 540 + testError := assert.AnError 541 + 542 + // Test error injection 543 + mock.SetError(testError) 544 + 545 + secret := createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator") 546 + 547 + // All operations should return the injected error 548 + err := mock.AddSecret(ctx, secret) 549 + assert.Equal(t, testError, err) 550 + 551 + _, err = mock.GetSecretsLocked(ctx, "did:plc:test/repo1") 552 + assert.Equal(t, testError, err) 553 + 554 + _, err = mock.GetSecretsUnlocked(ctx, "did:plc:test/repo1") 555 + assert.Equal(t, testError, err) 556 + 557 + err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: "did:plc:test/repo1"}) 558 + assert.Equal(t, testError, err) 559 + 560 + // Clear error and test normal operation 561 + mock.ClearError() 562 + err = mock.AddSecret(ctx, secret) 563 + assert.NoError(t, err) 564 + } 565 + 566 + func TestMockOpenBaoManager_Stop(t *testing.T) { 567 + mock := NewMockOpenBaoManager() 568 + 569 + assert.False(t, mock.IsStopped()) 570 + 571 + mock.Stop() 572 + 573 + assert.True(t, mock.IsStopped()) 574 + } 575 + 576 + func TestMockOpenBaoManager_Integration(t *testing.T) { 577 + tests := []struct { 578 + name string 579 + scenario func(t *testing.T, mock *MockOpenBaoManager) 580 + }{ 581 + { 582 + name: "complete workflow", 583 + scenario: func(t *testing.T, mock *MockOpenBaoManager) { 584 + ctx := context.Background() 585 + repo := DidSlashRepo("did:plc:test/integration") 586 + 587 + // Start with empty repo 588 + secrets, err := mock.GetSecretsLocked(ctx, repo) 589 + assert.NoError(t, err) 590 + assert.Empty(t, secrets) 591 + 592 + // Add some secrets 593 + secret1 := createTestSecretForOpenBao(string(repo), "API_KEY", "secret123", "did:plc:creator") 594 + secret2 := createTestSecretForOpenBao(string(repo), "DB_PASSWORD", "dbpass456", "did:plc:creator") 595 + 596 + err = mock.AddSecret(ctx, secret1) 597 + assert.NoError(t, err) 598 + 599 + err = mock.AddSecret(ctx, secret2) 600 + assert.NoError(t, err) 601 + 602 + // Verify secrets exist 603 + secrets, err = mock.GetSecretsLocked(ctx, repo) 604 + assert.NoError(t, err) 605 + assert.Len(t, secrets, 2) 606 + 607 + unlockedSecrets, err := mock.GetSecretsUnlocked(ctx, repo) 608 + assert.NoError(t, err) 609 + assert.Len(t, unlockedSecrets, 2) 610 + 611 + // Remove one secret 612 + err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: repo}) 613 + assert.NoError(t, err) 614 + 615 + // Verify only one secret remains 616 + secrets, err = mock.GetSecretsLocked(ctx, repo) 617 + assert.NoError(t, err) 618 + assert.Len(t, secrets, 1) 619 + assert.Equal(t, "DB_PASSWORD", secrets[0].Key) 620 + }, 621 + }, 622 + } 623 + 624 + for _, tt := range tests { 625 + t.Run(tt.name, func(t *testing.T) { 626 + mock := NewMockOpenBaoManager() 627 + tt.scenario(t, mock) 628 + }) 629 + } 630 + }
+15
spindle/secrets/policy.hcl
··· 1 + # KV v2 data operations 2 + path "spindle/data/*" { 3 + capabilities = ["create", "read", "update", "delete", "list"] 4 + } 5 + 6 + # KV v2 metadata operations (needed for listing) 7 + path "spindle/metadata/*" { 8 + capabilities = ["list", "read", "delete"] 9 + } 10 + 11 + # Root path access (needed for mount-level operations) 12 + path "spindle/*" { 13 + capabilities = ["list"] 14 + } 15 +
+9 -8
spindle/secrets/sqlite.go
··· 2 2 package secrets 3 3 4 4 import ( 5 + "context" 5 6 "database/sql" 6 7 "fmt" 7 8 "time" ··· 61 62 return err 62 63 } 63 64 64 - func (s *SqliteManager) AddSecret(secret UnlockedSecret) error { 65 + func (s *SqliteManager) AddSecret(ctx context.Context, secret UnlockedSecret) error { 65 66 query := fmt.Sprintf(` 66 67 insert or ignore into %s (repo, key, value, created_by) 67 68 values (?, ?, ?, ?); 68 69 `, s.tableName) 69 70 70 - res, err := s.db.Exec(query, secret.Repo, secret.Key, secret.Value, secret.CreatedBy) 71 + res, err := s.db.ExecContext(ctx, query, secret.Repo, secret.Key, secret.Value, secret.CreatedBy) 71 72 if err != nil { 72 73 return err 73 74 } ··· 84 85 return nil 85 86 } 86 87 87 - func (s *SqliteManager) RemoveSecret(secret Secret[any]) error { 88 + func (s *SqliteManager) RemoveSecret(ctx context.Context, secret Secret[any]) error { 88 89 query := fmt.Sprintf(` 89 90 delete from %s where repo = ? and key = ?; 90 91 `, s.tableName) 91 92 92 - res, err := s.db.Exec(query, secret.Repo, secret.Key) 93 + res, err := s.db.ExecContext(ctx, query, secret.Repo, secret.Key) 93 94 if err != nil { 94 95 return err 95 96 } ··· 106 107 return nil 107 108 } 108 109 109 - func (s *SqliteManager) GetSecretsLocked(didSlashRepo DidSlashRepo) ([]LockedSecret, error) { 110 + func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]LockedSecret, error) { 110 111 query := fmt.Sprintf(` 111 112 select repo, key, created_at, created_by from %s where repo = ?; 112 113 `, s.tableName) 113 114 114 - rows, err := s.db.Query(query, didSlashRepo) 115 + rows, err := s.db.QueryContext(ctx, query, didSlashRepo) 115 116 if err != nil { 116 117 return nil, err 117 118 } ··· 138 139 return ls, nil 139 140 } 140 141 141 - func (s *SqliteManager) GetSecretsUnlocked(didSlashRepo DidSlashRepo) ([]UnlockedSecret, error) { 142 + func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]UnlockedSecret, error) { 142 143 query := fmt.Sprintf(` 143 144 select repo, key, value, created_at, created_by from %s where repo = ?; 144 145 `, s.tableName) 145 146 146 - rows, err := s.db.Query(query, didSlashRepo) 147 + rows, err := s.db.QueryContext(ctx, query, didSlashRepo) 147 148 if err != nil { 148 149 return nil, err 149 150 }
+31 -21
spindle/secrets/sqlite_test.go
··· 1 1 package secrets 2 2 3 3 import ( 4 + "context" 4 5 "testing" 5 6 "time" 6 7 8 + "github.com/alecthomas/assert/v2" 7 9 "github.com/bluesky-social/indigo/atproto/syntax" 8 10 ) 9 11 ··· 122 124 defer manager.db.Close() 123 125 124 126 for i, secret := range tt.secrets { 125 - err := manager.AddSecret(secret) 127 + err := manager.AddSecret(context.Background(), secret) 126 128 if err != tt.expectError[i] { 127 129 t.Errorf("Secret %d: expected error %v, got %v", i, tt.expectError[i], err) 128 130 } ··· 189 191 190 192 // Setup secrets 191 193 for _, secret := range tt.setupSecrets { 192 - if err := manager.AddSecret(secret); err != nil { 194 + if err := manager.AddSecret(context.Background(), secret); err != nil { 193 195 t.Fatalf("Failed to setup secret: %v", err) 194 196 } 195 197 } 196 198 197 199 // Test removal 198 - err := manager.RemoveSecret(tt.removeSecret) 200 + err := manager.RemoveSecret(context.Background(), tt.removeSecret) 199 201 if err != tt.expectError { 200 202 t.Errorf("Expected error %v, got %v", tt.expectError, err) 201 203 } ··· 262 264 263 265 // Setup secrets 264 266 for _, secret := range tt.setupSecrets { 265 - if err := manager.AddSecret(secret); err != nil { 267 + if err := manager.AddSecret(context.Background(), secret); err != nil { 266 268 t.Fatalf("Failed to setup secret: %v", err) 267 269 } 268 270 } 269 271 270 272 // Test getting locked secrets 271 - lockedSecrets, err := manager.GetSecretsLocked(tt.queryRepo) 273 + lockedSecrets, err := manager.GetSecretsLocked(context.Background(), tt.queryRepo) 272 274 if tt.expectError && err == nil { 273 275 t.Error("Expected error but got none") 274 276 return ··· 369 371 370 372 // Setup secrets 371 373 for _, secret := range tt.setupSecrets { 372 - if err := manager.AddSecret(secret); err != nil { 374 + if err := manager.AddSecret(context.Background(), secret); err != nil { 373 375 t.Fatalf("Failed to setup secret: %v", err) 374 376 } 375 377 } 376 378 377 379 // Test getting unlocked secrets 378 - unlockedSecrets, err := manager.GetSecretsUnlocked(tt.queryRepo) 380 + unlockedSecrets, err := manager.GetSecretsUnlocked(context.Background(), tt.queryRepo) 379 381 if tt.expectError && err == nil { 380 382 t.Error("Expected error but got none") 381 383 return ··· 424 426 operations: []func(Manager) error{ 425 427 func(m Manager) error { 426 428 secret := createTestSecret("interface.test/repo", "test_key", "test_value", "did:plc:user") 427 - return m.AddSecret(secret) 429 + return m.AddSecret(context.Background(), secret) 428 430 }, 429 431 func(m Manager) error { 430 - _, err := m.GetSecretsLocked(DidSlashRepo("interface.test/repo")) 432 + _, err := m.GetSecretsLocked(context.Background(), DidSlashRepo("interface.test/repo")) 431 433 return err 432 434 }, 433 435 func(m Manager) error { 434 - _, err := m.GetSecretsUnlocked(DidSlashRepo("interface.test/repo")) 436 + _, err := m.GetSecretsUnlocked(context.Background(), DidSlashRepo("interface.test/repo")) 435 437 return err 436 438 }, 437 439 func(m Manager) error { ··· 439 441 Key: "test_key", 440 442 Repo: DidSlashRepo("interface.test/repo"), 441 443 } 442 - return m.RemoveSecret(secret) 444 + return m.RemoveSecret(context.Background(), secret) 443 445 }, 444 446 }, 445 447 expectError: false, ··· 449 451 operations: []func(Manager) error{ 450 452 func(m Manager) error { 451 453 secret := createTestSecret("interface.test/repo", "dup_key", "value1", "did:plc:user") 452 - return m.AddSecret(secret) 454 + return m.AddSecret(context.Background(), secret) 453 455 }, 454 456 func(m Manager) error { 455 457 secret := createTestSecret("interface.test/repo", "dup_key", "value2", "did:plc:user") 456 - return m.AddSecret(secret) // Should return ErrKeyAlreadyPresent 458 + return m.AddSecret(context.Background(), secret) // Should return ErrKeyAlreadyPresent 457 459 }, 458 460 }, 459 461 expectError: true, ··· 507 509 508 510 // Add all secrets 509 511 for _, secret := range secrets { 510 - if err := manager.AddSecret(secret); err != nil { 512 + if err := manager.AddSecret(context.Background(), secret); err != nil { 511 513 t.Fatalf("Failed to add secret %s: %v", secret.Key, err) 512 514 } 513 515 } 514 516 515 517 // Verify counts 516 - locked1, _ := manager.GetSecretsLocked(repo1) 517 - locked2, _ := manager.GetSecretsLocked(repo2) 518 + locked1, _ := manager.GetSecretsLocked(context.Background(), repo1) 519 + locked2, _ := manager.GetSecretsLocked(context.Background(), repo2) 518 520 519 521 if len(locked1) != 2 { 520 522 t.Errorf("Expected 2 secrets for repo1, got %d", len(locked1)) ··· 525 527 526 528 // Remove and verify 527 529 secretToRemove := Secret[any]{Key: "db_password", Repo: repo1} 528 - if err := manager.RemoveSecret(secretToRemove); err != nil { 530 + if err := manager.RemoveSecret(context.Background(), secretToRemove); err != nil { 529 531 t.Fatalf("Failed to remove secret: %v", err) 530 532 } 531 533 532 - locked1After, _ := manager.GetSecretsLocked(repo1) 534 + locked1After, _ := manager.GetSecretsLocked(context.Background(), repo1) 533 535 if len(locked1After) != 1 { 534 536 t.Errorf("Expected 1 secret for repo1 after removal, got %d", len(locked1After)) 535 537 } ··· 544 546 repo := DidSlashRepo("empty.test/repo") 545 547 546 548 // Operations on empty database should not error 547 - locked, err := manager.GetSecretsLocked(repo) 549 + locked, err := manager.GetSecretsLocked(context.Background(), repo) 548 550 if err != nil { 549 551 t.Errorf("GetSecretsLocked on empty DB failed: %v", err) 550 552 } ··· 552 554 t.Errorf("Expected 0 secrets, got %d", len(locked)) 553 555 } 554 556 555 - unlocked, err := manager.GetSecretsUnlocked(repo) 557 + unlocked, err := manager.GetSecretsUnlocked(context.Background(), repo) 556 558 if err != nil { 557 559 t.Errorf("GetSecretsUnlocked on empty DB failed: %v", err) 558 560 } ··· 562 564 563 565 // Remove from empty should return ErrKeyNotFound 564 566 nonExistent := Secret[any]{Key: "none", Repo: repo} 565 - err = manager.RemoveSecret(nonExistent) 567 + err = manager.RemoveSecret(context.Background(), nonExistent) 566 568 if err != ErrKeyNotFound { 567 569 t.Errorf("Expected ErrKeyNotFound, got %v", err) 568 570 } ··· 578 580 }) 579 581 } 580 582 } 583 + 584 + func TestSqliteManager_StopperInterface(t *testing.T) { 585 + manager := &SqliteManager{} 586 + 587 + // Verify that SqliteManager does NOT implement the Stopper interface 588 + _, ok := interface{}(manager).(Stopper) 589 + assert.False(t, ok, "SqliteManager should NOT implement Stopper interface") 590 + }
+36 -4
spindle/server.go
··· 68 68 69 69 n := notifier.New() 70 70 71 - // TODO: add hashicorp vault provider and choose here 72 - vault, err := secrets.NewSQLiteManager(cfg.Server.DBPath, secrets.WithTableName("secrets")) 73 - if err != nil { 74 - return fmt.Errorf("failed to setup secrets provider: %w", err) 71 + var vault secrets.Manager 72 + switch cfg.Server.Secrets.Provider { 73 + case "openbao": 74 + if cfg.Server.Secrets.OpenBao.Addr == "" { 75 + return fmt.Errorf("openbao address is required when using openbao secrets provider") 76 + } 77 + if cfg.Server.Secrets.OpenBao.RoleID == "" { 78 + return fmt.Errorf("openbao role_id is required when using openbao secrets provider") 79 + } 80 + if cfg.Server.Secrets.OpenBao.SecretID == "" { 81 + return fmt.Errorf("openbao secret_id is required when using openbao secrets provider") 82 + } 83 + vault, err = secrets.NewOpenBaoManager( 84 + cfg.Server.Secrets.OpenBao.Addr, 85 + cfg.Server.Secrets.OpenBao.RoleID, 86 + cfg.Server.Secrets.OpenBao.SecretID, 87 + logger, 88 + secrets.WithMountPath(cfg.Server.Secrets.OpenBao.Mount), 89 + ) 90 + if err != nil { 91 + return fmt.Errorf("failed to setup openbao secrets provider: %w", err) 92 + } 93 + logger.Info("using openbao secrets provider", "address", cfg.Server.Secrets.OpenBao.Addr, "mount", cfg.Server.Secrets.OpenBao.Mount) 94 + case "sqlite", "": 95 + vault, err = secrets.NewSQLiteManager(cfg.Server.DBPath, secrets.WithTableName("secrets")) 96 + if err != nil { 97 + return fmt.Errorf("failed to setup sqlite secrets provider: %w", err) 98 + } 99 + logger.Info("using sqlite secrets provider", "path", cfg.Server.DBPath) 100 + default: 101 + return fmt.Errorf("unknown secrets provider: %s", cfg.Server.Secrets.Provider) 75 102 } 76 103 77 104 eng, err := engine.New(ctx, cfg, d, &n, vault) ··· 120 147 jq.Start() 121 148 defer jq.Stop() 122 149 150 + // Stop vault token renewal if it implements Stopper 151 + if stopper, ok := vault.(secrets.Stopper); ok { 152 + defer stopper.Stop() 153 + } 154 + 123 155 cursorStore, err := cursor.NewSQLiteStore(cfg.Server.DBPath) 124 156 if err != nil { 125 157 return fmt.Errorf("failed to setup sqlite3 cursor store: %w", err)