+31
-13
go.mod
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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)
···
119
146
// starts a job queue runner in the background
120
147
jq.Start()
121
148
defer jq.Stop()
149
+
150
+
// Stop vault token renewal if it implements Stopper
151
+
if stopper, ok := vault.(secrets.Stopper); ok {
152
+
defer stopper.Stop()
153
+
}
122
154
123
155
cursorStore, err := cursor.NewSQLiteStore(cfg.Server.DBPath)
124
156
if err != nil {