+11
-7
spindle/server.go
+11
-7
spindle/server.go
···
25
"tangled.sh/tangled.sh/core/spindle/queue"
26
"tangled.sh/tangled.sh/core/spindle/secrets"
27
"tangled.sh/tangled.sh/core/spindle/xrpc"
28
)
29
30
//go:embed motd
···
213
func (s *Spindle) XrpcRouter() http.Handler {
214
logger := s.l.With("route", "xrpc")
215
216
x := xrpc.Xrpc{
217
-
Logger: logger,
218
-
Db: s.db,
219
-
Enforcer: s.e,
220
-
Engines: s.engs,
221
-
Config: s.cfg,
222
-
Resolver: s.res,
223
-
Vault: s.vault,
224
}
225
226
return x.Router()
···
25
"tangled.sh/tangled.sh/core/spindle/queue"
26
"tangled.sh/tangled.sh/core/spindle/secrets"
27
"tangled.sh/tangled.sh/core/spindle/xrpc"
28
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
29
)
30
31
//go:embed motd
···
214
func (s *Spindle) XrpcRouter() http.Handler {
215
logger := s.l.With("route", "xrpc")
216
217
+
serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String())
218
+
219
x := xrpc.Xrpc{
220
+
Logger: logger,
221
+
Db: s.db,
222
+
Enforcer: s.e,
223
+
Engines: s.engs,
224
+
Config: s.cfg,
225
+
Resolver: s.res,
226
+
Vault: s.vault,
227
+
ServiceAuth: serviceAuth,
228
}
229
230
return x.Router()
+11
-10
spindle/xrpc/add_secret.go
+11
-10
spindle/xrpc/add_secret.go
···
13
"tangled.sh/tangled.sh/core/api/tangled"
14
"tangled.sh/tangled.sh/core/rbac"
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
)
17
18
func (x *Xrpc) AddSecret(w http.ResponseWriter, r *http.Request) {
19
l := x.Logger
20
-
fail := func(e XrpcError) {
21
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
writeError(w, e, http.StatusBadRequest)
23
}
24
25
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
if !ok {
27
-
fail(MissingActorDidError)
28
return
29
}
30
31
var data tangled.RepoAddSecret_Input
32
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
33
-
fail(GenericError(err))
34
return
35
}
36
37
if err := secrets.ValidateKey(data.Key); err != nil {
38
-
fail(GenericError(err))
39
return
40
}
41
42
// unfortunately we have to resolve repo-at here
43
repoAt, err := syntax.ParseATURI(data.Repo)
44
if err != nil {
45
-
fail(InvalidRepoError(data.Repo))
46
return
47
}
48
49
// resolve this aturi to extract the repo record
50
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
51
if err != nil || ident.Handle.IsInvalidHandle() {
52
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
53
return
54
}
55
56
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
57
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
58
if err != nil {
59
-
fail(GenericError(err))
60
return
61
}
62
63
repo := resp.Value.Val.(*tangled.Repo)
64
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
65
if err != nil {
66
-
fail(GenericError(err))
67
return
68
}
69
70
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
71
l.Error("insufficent permissions", "did", actorDid.String())
72
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
73
return
74
}
75
···
83
err = x.Vault.AddSecret(r.Context(), secret)
84
if err != nil {
85
l.Error("failed to add secret to vault", "did", actorDid.String(), "err", err)
86
-
writeError(w, GenericError(err), http.StatusInternalServerError)
87
return
88
}
89
···
13
"tangled.sh/tangled.sh/core/api/tangled"
14
"tangled.sh/tangled.sh/core/rbac"
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
17
)
18
19
func (x *Xrpc) AddSecret(w http.ResponseWriter, r *http.Request) {
20
l := x.Logger
21
+
fail := func(e xrpcerr.XrpcError) {
22
l.Error("failed", "kind", e.Tag, "error", e.Message)
23
writeError(w, e, http.StatusBadRequest)
24
}
25
26
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27
if !ok {
28
+
fail(xrpcerr.MissingActorDidError)
29
return
30
}
31
32
var data tangled.RepoAddSecret_Input
33
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34
+
fail(xrpcerr.GenericError(err))
35
return
36
}
37
38
if err := secrets.ValidateKey(data.Key); err != nil {
39
+
fail(xrpcerr.GenericError(err))
40
return
41
}
42
43
// unfortunately we have to resolve repo-at here
44
repoAt, err := syntax.ParseATURI(data.Repo)
45
if err != nil {
46
+
fail(xrpcerr.InvalidRepoError(data.Repo))
47
return
48
}
49
50
// resolve this aturi to extract the repo record
51
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
52
if err != nil || ident.Handle.IsInvalidHandle() {
53
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
54
return
55
}
56
57
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
58
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
59
if err != nil {
60
+
fail(xrpcerr.GenericError(err))
61
return
62
}
63
64
repo := resp.Value.Val.(*tangled.Repo)
65
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
66
if err != nil {
67
+
fail(xrpcerr.GenericError(err))
68
return
69
}
70
71
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
72
l.Error("insufficent permissions", "did", actorDid.String())
73
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
74
return
75
}
76
···
84
err = x.Vault.AddSecret(r.Context(), secret)
85
if err != nil {
86
l.Error("failed to add secret to vault", "did", actorDid.String(), "err", err)
87
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
88
return
89
}
90
+10
-9
spindle/xrpc/list_secrets.go
+10
-9
spindle/xrpc/list_secrets.go
···
13
"tangled.sh/tangled.sh/core/api/tangled"
14
"tangled.sh/tangled.sh/core/rbac"
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
)
17
18
func (x *Xrpc) ListSecrets(w http.ResponseWriter, r *http.Request) {
19
l := x.Logger
20
-
fail := func(e XrpcError) {
21
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
writeError(w, e, http.StatusBadRequest)
23
}
24
25
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
if !ok {
27
-
fail(MissingActorDidError)
28
return
29
}
30
31
repoParam := r.URL.Query().Get("repo")
32
if repoParam == "" {
33
-
fail(GenericError(fmt.Errorf("empty params")))
34
return
35
}
36
37
// unfortunately we have to resolve repo-at here
38
repoAt, err := syntax.ParseATURI(repoParam)
39
if err != nil {
40
-
fail(InvalidRepoError(repoParam))
41
return
42
}
43
44
// resolve this aturi to extract the repo record
45
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
46
if err != nil || ident.Handle.IsInvalidHandle() {
47
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
48
return
49
}
50
51
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
52
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
53
if err != nil {
54
-
fail(GenericError(err))
55
return
56
}
57
58
repo := resp.Value.Val.(*tangled.Repo)
59
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
60
if err != nil {
61
-
fail(GenericError(err))
62
return
63
}
64
65
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
return
69
}
70
71
ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath))
72
if err != nil {
73
l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err)
74
-
writeError(w, GenericError(err), http.StatusInternalServerError)
75
return
76
}
77
···
13
"tangled.sh/tangled.sh/core/api/tangled"
14
"tangled.sh/tangled.sh/core/rbac"
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
17
)
18
19
func (x *Xrpc) ListSecrets(w http.ResponseWriter, r *http.Request) {
20
l := x.Logger
21
+
fail := func(e xrpcerr.XrpcError) {
22
l.Error("failed", "kind", e.Tag, "error", e.Message)
23
writeError(w, e, http.StatusBadRequest)
24
}
25
26
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27
if !ok {
28
+
fail(xrpcerr.MissingActorDidError)
29
return
30
}
31
32
repoParam := r.URL.Query().Get("repo")
33
if repoParam == "" {
34
+
fail(xrpcerr.GenericError(fmt.Errorf("empty params")))
35
return
36
}
37
38
// unfortunately we have to resolve repo-at here
39
repoAt, err := syntax.ParseATURI(repoParam)
40
if err != nil {
41
+
fail(xrpcerr.InvalidRepoError(repoParam))
42
return
43
}
44
45
// resolve this aturi to extract the repo record
46
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
47
if err != nil || ident.Handle.IsInvalidHandle() {
48
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
49
return
50
}
51
52
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
53
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
54
if err != nil {
55
+
fail(xrpcerr.GenericError(err))
56
return
57
}
58
59
repo := resp.Value.Val.(*tangled.Repo)
60
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
61
if err != nil {
62
+
fail(xrpcerr.GenericError(err))
63
return
64
}
65
66
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
67
l.Error("insufficent permissions", "did", actorDid.String())
68
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
return
70
}
71
72
ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath))
73
if err != nil {
74
l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err)
75
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
76
return
77
}
78
+10
-9
spindle/xrpc/remove_secret.go
+10
-9
spindle/xrpc/remove_secret.go
···
12
"tangled.sh/tangled.sh/core/api/tangled"
13
"tangled.sh/tangled.sh/core/rbac"
14
"tangled.sh/tangled.sh/core/spindle/secrets"
15
)
16
17
func (x *Xrpc) RemoveSecret(w http.ResponseWriter, r *http.Request) {
18
l := x.Logger
19
-
fail := func(e XrpcError) {
20
l.Error("failed", "kind", e.Tag, "error", e.Message)
21
writeError(w, e, http.StatusBadRequest)
22
}
23
24
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
if !ok {
26
-
fail(MissingActorDidError)
27
return
28
}
29
30
var data tangled.RepoRemoveSecret_Input
31
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
32
-
fail(GenericError(err))
33
return
34
}
35
36
// unfortunately we have to resolve repo-at here
37
repoAt, err := syntax.ParseATURI(data.Repo)
38
if err != nil {
39
-
fail(InvalidRepoError(data.Repo))
40
return
41
}
42
43
// resolve this aturi to extract the repo record
44
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
45
if err != nil || ident.Handle.IsInvalidHandle() {
46
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
47
return
48
}
49
50
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
51
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
52
if err != nil {
53
-
fail(GenericError(err))
54
return
55
}
56
57
repo := resp.Value.Val.(*tangled.Repo)
58
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
59
if err != nil {
60
-
fail(GenericError(err))
61
return
62
}
63
64
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
65
l.Error("insufficent permissions", "did", actorDid.String())
66
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
67
return
68
}
69
···
74
err = x.Vault.RemoveSecret(r.Context(), secret)
75
if err != nil {
76
l.Error("failed to remove secret from vault", "did", actorDid.String(), "err", err)
77
-
writeError(w, GenericError(err), http.StatusInternalServerError)
78
return
79
}
80
···
12
"tangled.sh/tangled.sh/core/api/tangled"
13
"tangled.sh/tangled.sh/core/rbac"
14
"tangled.sh/tangled.sh/core/spindle/secrets"
15
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
)
17
18
func (x *Xrpc) RemoveSecret(w http.ResponseWriter, r *http.Request) {
19
l := x.Logger
20
+
fail := func(e xrpcerr.XrpcError) {
21
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
writeError(w, e, http.StatusBadRequest)
23
}
24
25
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
if !ok {
27
+
fail(xrpcerr.MissingActorDidError)
28
return
29
}
30
31
var data tangled.RepoRemoveSecret_Input
32
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
33
+
fail(xrpcerr.GenericError(err))
34
return
35
}
36
37
// unfortunately we have to resolve repo-at here
38
repoAt, err := syntax.ParseATURI(data.Repo)
39
if err != nil {
40
+
fail(xrpcerr.InvalidRepoError(data.Repo))
41
return
42
}
43
44
// resolve this aturi to extract the repo record
45
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
46
if err != nil || ident.Handle.IsInvalidHandle() {
47
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
48
return
49
}
50
51
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
52
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
53
if err != nil {
54
+
fail(xrpcerr.GenericError(err))
55
return
56
}
57
58
repo := resp.Value.Val.(*tangled.Repo)
59
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
60
if err != nil {
61
+
fail(xrpcerr.GenericError(err))
62
return
63
}
64
65
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
return
69
}
70
···
75
err = x.Vault.RemoveSecret(r.Context(), secret)
76
if err != nil {
77
l.Error("failed to remove secret from vault", "did", actorDid.String(), "err", err)
78
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
79
return
80
}
81
+14
-109
spindle/xrpc/xrpc.go
+14
-109
spindle/xrpc/xrpc.go
···
1
package xrpc
2
3
import (
4
-
"context"
5
_ "embed"
6
"encoding/json"
7
-
"fmt"
8
"log/slog"
9
"net/http"
10
-
"strings"
11
12
-
"github.com/bluesky-social/indigo/atproto/auth"
13
"github.com/go-chi/chi/v5"
14
15
"tangled.sh/tangled.sh/core/api/tangled"
···
19
"tangled.sh/tangled.sh/core/spindle/db"
20
"tangled.sh/tangled.sh/core/spindle/models"
21
"tangled.sh/tangled.sh/core/spindle/secrets"
22
)
23
24
const ActorDid string = "ActorDid"
25
26
type Xrpc struct {
27
-
Logger *slog.Logger
28
-
Db *db.DB
29
-
Enforcer *rbac.Enforcer
30
-
Engines map[string]models.Engine
31
-
Config *config.Config
32
-
Resolver *idresolver.Resolver
33
-
Vault secrets.Manager
34
}
35
36
func (x *Xrpc) Router() http.Handler {
37
r := chi.NewRouter()
38
39
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
40
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
41
-
r.With(x.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
42
43
return r
44
}
45
46
-
func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler {
47
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
48
-
l := x.Logger.With("url", r.URL)
49
-
50
-
token := r.Header.Get("Authorization")
51
-
token = strings.TrimPrefix(token, "Bearer ")
52
-
53
-
s := auth.ServiceAuthValidator{
54
-
Audience: x.Config.Server.Did().String(),
55
-
Dir: x.Resolver.Directory(),
56
-
}
57
-
58
-
did, err := s.Validate(r.Context(), token, nil)
59
-
if err != nil {
60
-
l.Error("signature verification failed", "err", err)
61
-
writeError(w, AuthError(err), http.StatusForbidden)
62
-
return
63
-
}
64
-
65
-
r = r.WithContext(
66
-
context.WithValue(r.Context(), ActorDid, did),
67
-
)
68
-
69
-
next.ServeHTTP(w, r)
70
-
})
71
-
}
72
-
73
-
type XrpcError struct {
74
-
Tag string `json:"error"`
75
-
Message string `json:"message"`
76
-
}
77
-
78
-
func NewXrpcError(opts ...ErrOpt) XrpcError {
79
-
x := XrpcError{}
80
-
for _, o := range opts {
81
-
o(&x)
82
-
}
83
-
84
-
return x
85
-
}
86
-
87
-
type ErrOpt = func(xerr *XrpcError)
88
-
89
-
func WithTag(tag string) ErrOpt {
90
-
return func(xerr *XrpcError) {
91
-
xerr.Tag = tag
92
-
}
93
-
}
94
-
95
-
func WithMessage[S ~string](s S) ErrOpt {
96
-
return func(xerr *XrpcError) {
97
-
xerr.Message = string(s)
98
-
}
99
-
}
100
-
101
-
func WithError(e error) ErrOpt {
102
-
return func(xerr *XrpcError) {
103
-
xerr.Message = e.Error()
104
-
}
105
-
}
106
-
107
-
var MissingActorDidError = NewXrpcError(
108
-
WithTag("MissingActorDid"),
109
-
WithMessage("actor DID not supplied"),
110
-
)
111
-
112
-
var AuthError = func(err error) XrpcError {
113
-
return NewXrpcError(
114
-
WithTag("Auth"),
115
-
WithError(fmt.Errorf("signature verification failed: %w", err)),
116
-
)
117
-
}
118
-
119
-
var InvalidRepoError = func(r string) XrpcError {
120
-
return NewXrpcError(
121
-
WithTag("InvalidRepo"),
122
-
WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),
123
-
)
124
-
}
125
-
126
-
func GenericError(err error) XrpcError {
127
-
return NewXrpcError(
128
-
WithTag("Generic"),
129
-
WithError(err),
130
-
)
131
-
}
132
-
133
-
var AccessControlError = func(d string) XrpcError {
134
-
return NewXrpcError(
135
-
WithTag("AccessControl"),
136
-
WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),
137
-
)
138
-
}
139
-
140
// this is slightly different from http_util::write_error to follow the spec:
141
//
142
// the json object returned must include an "error" and a "message"
143
-
func writeError(w http.ResponseWriter, e XrpcError, status int) {
144
w.Header().Set("Content-Type", "application/json")
145
w.WriteHeader(status)
146
json.NewEncoder(w).Encode(e)
···
1
package xrpc
2
3
import (
4
_ "embed"
5
"encoding/json"
6
"log/slog"
7
"net/http"
8
9
"github.com/go-chi/chi/v5"
10
11
"tangled.sh/tangled.sh/core/api/tangled"
···
15
"tangled.sh/tangled.sh/core/spindle/db"
16
"tangled.sh/tangled.sh/core/spindle/models"
17
"tangled.sh/tangled.sh/core/spindle/secrets"
18
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
19
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
20
)
21
22
const ActorDid string = "ActorDid"
23
24
type Xrpc struct {
25
+
Logger *slog.Logger
26
+
Db *db.DB
27
+
Enforcer *rbac.Enforcer
28
+
Engines map[string]models.Engine
29
+
Config *config.Config
30
+
Resolver *idresolver.Resolver
31
+
Vault secrets.Manager
32
+
ServiceAuth *serviceauth.ServiceAuth
33
}
34
35
func (x *Xrpc) Router() http.Handler {
36
r := chi.NewRouter()
37
38
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
39
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
40
+
r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
41
42
return r
43
}
44
45
// this is slightly different from http_util::write_error to follow the spec:
46
//
47
// the json object returned must include an "error" and a "message"
48
+
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
49
w.Header().Set("Content-Type", "application/json")
50
w.WriteHeader(status)
51
json.NewEncoder(w).Encode(e)