diff --git a/appview/db/db.go b/appview/db/db.go index 99139418..e574cb7b 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -703,6 +703,37 @@ func Make(dbPath string) (*DB, error) { return err }) + // repurpose the read-only column to "needs-upgrade" + runMigration(conn, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { + _, err := tx.Exec(` + alter table registrations rename column read_only to needs_upgrade; + `) + return err + }) + + // require all knots to upgrade after the release of total xrpc + runMigration(conn, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { + _, err := tx.Exec(` + update registrations set needs_upgrade = 1; + `) + return err + }) + + // require all knots to upgrade after the release of total xrpc + runMigration(conn, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { + _, err := tx.Exec(` + alter table spindles add column needs_upgrade integer not null default 0; + `) + if err != nil { + return err + } + + _, err = tx.Exec(` + update spindles set needs_upgrade = 1; + `) + return err + }) + return &DB{db}, nil } diff --git a/appview/db/registration.go b/appview/db/registration.go index 9c66cb07..371ee04e 100644 --- a/appview/db/registration.go +++ b/appview/db/registration.go @@ -10,17 +10,17 @@ import ( // Registration represents a knot registration. Knot would've been a better // name but we're stuck with this for historical reasons. type Registration struct { - Id int64 - Domain string - ByDid string - Created *time.Time - Registered *time.Time - ReadOnly bool + Id int64 + Domain string + ByDid string + Created *time.Time + Registered *time.Time + NeedsUpgrade bool } func (r *Registration) Status() Status { - if r.ReadOnly { - return ReadOnly + if r.NeedsUpgrade { + return NeedsUpgrade } else if r.Registered != nil { return Registered } else { @@ -32,8 +32,8 @@ func (r *Registration) IsRegistered() bool { return r.Status() == Registered } -func (r *Registration) IsReadOnly() bool { - return r.Status() == ReadOnly +func (r *Registration) IsNeedsUpgrade() bool { + return r.Status() == NeedsUpgrade } func (r *Registration) IsPending() bool { @@ -45,7 +45,7 @@ type Status uint32 const ( Registered Status = iota Pending - ReadOnly + NeedsUpgrade ) func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { @@ -64,7 +64,7 @@ func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { } query := fmt.Sprintf(` - select id, domain, did, created, registered, read_only + select id, domain, did, created, registered, needs_upgrade from registrations %s order by created @@ -80,10 +80,10 @@ func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { for rows.Next() { var createdAt string var registeredAt sql.Null[string] - var readOnly int + var needsUpgrade int var reg Registration - err = rows.Scan(®.Id, ®.Domain, ®.ByDid, &createdAt, ®isteredAt, &readOnly) + err = rows.Scan(®.Id, ®.Domain, ®.ByDid, &createdAt, ®isteredAt, &needsUpgrade) if err != nil { return nil, err } @@ -98,8 +98,8 @@ func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { } } - if readOnly != 0 { - reg.ReadOnly = true + if needsUpgrade != 0 { + reg.NeedsUpgrade = true } registrations = append(registrations, reg) @@ -116,7 +116,7 @@ func MarkRegistered(e Execer, filters ...filter) error { args = append(args, filter.Arg()...) } - query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0" + query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), needs_upgrade = 0" if len(conditions) > 0 { query += " where " + strings.Join(conditions, " and ") } diff --git a/appview/db/spindle.go b/appview/db/spindle.go index f4e037fa..d587fcfe 100644 --- a/appview/db/spindle.go +++ b/appview/db/spindle.go @@ -10,11 +10,12 @@ import ( ) type Spindle struct { - Id int - Owner syntax.DID - Instance string - Verified *time.Time - Created time.Time + Id int + Owner syntax.DID + Instance string + Verified *time.Time + Created time.Time + NeedsUpgrade bool } type SpindleMember struct { @@ -42,7 +43,7 @@ func GetSpindles(e Execer, filters ...filter) ([]Spindle, error) { } query := fmt.Sprintf( - `select id, owner, instance, verified, created + `select id, owner, instance, verified, created, needs_upgrade from spindles %s order by created @@ -61,6 +62,7 @@ func GetSpindles(e Execer, filters ...filter) ([]Spindle, error) { var spindle Spindle var createdAt string var verified sql.NullString + var needsUpgrade int if err := rows.Scan( &spindle.Id, @@ -68,6 +70,7 @@ func GetSpindles(e Execer, filters ...filter) ([]Spindle, error) { &spindle.Instance, &verified, &createdAt, + &needsUpgrade, ); err != nil { return nil, err } @@ -86,6 +89,10 @@ func GetSpindles(e Execer, filters ...filter) ([]Spindle, error) { spindle.Verified = &t } + if needsUpgrade != 0 { + spindle.NeedsUpgrade = true + } + spindles = append(spindles, spindle) } @@ -115,7 +122,7 @@ func VerifySpindle(e Execer, filters ...filter) (int64, error) { whereClause = " where " + strings.Join(conditions, " and ") } - query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) + query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now'), needs_upgrade = 0 %s`, whereClause) res, err := e.Exec(query, args...) if err != nil { diff --git a/appview/knots/knots.go b/appview/knots/knots.go index 9487a9f2..24e4bbe0 100644 --- a/appview/knots/knots.go +++ b/appview/knots/knots.go @@ -3,7 +3,6 @@ package knots import ( "errors" "fmt" - "log" "log/slog" "net/http" "slices" @@ -17,6 +16,7 @@ import ( "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" "tangled.sh/tangled.sh/core/appview/serververify" + "tangled.sh/tangled.sh/core/appview/xrpcclient" "tangled.sh/tangled.sh/core/eventconsumer" "tangled.sh/tangled.sh/core/idresolver" "tangled.sh/tangled.sh/core/rbac" @@ -50,8 +50,6 @@ func (k *Knots) Router() http.Handler { r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember) r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember) - r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner) - return r } @@ -399,8 +397,8 @@ func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { if err != nil { l.Error("verification failed", "err", err) - if errors.Is(err, serververify.FetchError) { - k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") + if errors.Is(err, xrpcclient.ErrXrpcUnsupported) { + k.Pages.Notice(w, noticeId, "Failed to verify knot, XRPC queries are unsupported on this knot, consider upgrading!") return } @@ -420,10 +418,10 @@ func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { return } - // if this knot was previously read-only, then emit a record too + // if this knot requires upgrade, then emit a record too // // this is part of migrating from the old knot system to the new one - if registration.ReadOnly { + if registration.NeedsUpgrade { // re-announce by registering under same rkey client, err := k.OAuth.AuthorizedClient(r) if err != nil { @@ -485,8 +483,6 @@ func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { } updatedRegistration := registrations[0] - log.Println(updatedRegistration) - w.Header().Set("HX-Reswap", "outerHTML") k.Pages.KnotListing(w, pages.KnotListingParams{ Registration: &updatedRegistration, @@ -678,51 +674,3 @@ func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { // ok k.Pages.HxRefresh(w) } - -func (k *Knots) banner(w http.ResponseWriter, r *http.Request) { - user := k.OAuth.GetUser(r) - l := k.Logger.With("handler", "banner") - l = l.With("did", user.Did) - l = l.With("handle", user.Handle) - - allRegistrations, err := db.GetRegistrations( - k.Db, - db.FilterEq("did", user.Did), - ) - if err != nil { - l.Error("non-fatal: failed to get registrations") - return - } - - httpClient := &http.Client{Timeout: 5 * time.Second} - regs404 := []db.Registration{} - for _, reg := range allRegistrations { - healthURL := fmt.Sprintf("http://%s/xrpc/_health", reg.Domain) - - fmt.Println(healthURL) - - req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, healthURL, nil) - if err != nil { - l.Error("failed to create health check request", "domain", reg.Domain, "err", err) - continue - } - - resp, err := httpClient.Do(req) - if err != nil { - l.Error("failed to make health check request", "domain", reg.Domain, "err", err) - continue - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusNotFound { - regs404 = append(regs404, reg) - } - } - if len(regs404) == 0 { - return - } - - k.Pages.KnotBanner(w, pages.KnotBannerParams{ - Registrations: regs404, - }) -} diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 5340a86c..187cace0 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -351,12 +351,13 @@ func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) return p.execute("user/settings/emails", w, params) } -type KnotBannerParams struct { +type UpgradeBannerParams struct { Registrations []db.Registration + Spindles []db.Spindle } -func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { - return p.executePlain("knots/fragments/bannerRequiresUpgrade", w, params) +func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { + return p.executePlain("banner", w, params) } type KnotsParams struct { diff --git a/appview/pages/templates/banner.html b/appview/pages/templates/banner.html new file mode 100644 index 00000000..99dfad61 --- /dev/null +++ b/appview/pages/templates/banner.html @@ -0,0 +1,23 @@ +{{ define "banner" }} +
+ The following services that you administer will have to be upgraded to be compatible with the latest version of Tangled: + {{ if .Registrations }} + + Repositories hosted on these knots may not be accessible until upgraded. + {{ end }} + + {{ if .Spindles }} + + Pipelines may not be executed on these spindles until upgraded. + {{ end }} + Click to read the upgrade guide. +
+{{ end }} diff --git a/appview/pages/templates/knots/fragments/bannerReadOnly.html b/appview/pages/templates/knots/fragments/bannerReadOnly.html deleted file mode 100644 index 1f200d79..00000000 --- a/appview/pages/templates/knots/fragments/bannerReadOnly.html +++ /dev/null @@ -1,8 +0,0 @@ -{{ define "knots/fragments/bannerReadOnly" }} -
- A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }}) - that you administer is presently read-only. Consider upgrading this knot to - continue creating repositories on it. - Click to read the upgrade guide. -
-{{ end }} diff --git a/appview/pages/templates/knots/fragments/bannerRequiresUpgrade.html b/appview/pages/templates/knots/fragments/bannerRequiresUpgrade.html deleted file mode 100644 index 0efbdb1d..00000000 --- a/appview/pages/templates/knots/fragments/bannerRequiresUpgrade.html +++ /dev/null @@ -1,12 +0,0 @@ -{{ define "knots/fragments/bannerRequiresUpgrade" }} -
- The following knots that you administer will have to be upgraded to be compatible with the latest version of Tangled: - - Repositories hosted on these knots will not be accessible until upgraded. - Click to read the upgrade guide. -
-{{ end }} diff --git a/appview/pages/templates/knots/fragments/knotListing.html b/appview/pages/templates/knots/fragments/knotListing.html index 6842e03c..5cdc86d9 100644 --- a/appview/pages/templates/knots/fragments/knotListing.html +++ b/appview/pages/templates/knots/fragments/knotListing.html @@ -36,9 +36,9 @@ {{ template "knots/fragments/addMemberModal" . }} {{ block "knotDeleteButton" . }} {{ end }} - {{ else if .IsReadOnly }} + {{ else if .IsNeedsUpgrade }} - {{ i "shield-alert" "w-4 h-4" }} read-only + {{ i "shield-alert" "w-4 h-4" }} needs upgrade {{ block "knotRetryButton" . }} {{ end }} {{ block "knotDeleteButton" . }} {{ end }} diff --git a/appview/pages/templates/layouts/base.html b/appview/pages/templates/layouts/base.html index 691a68ad..0d35dd0e 100644 --- a/appview/pages/templates/layouts/base.html +++ b/appview/pages/templates/layouts/base.html @@ -53,7 +53,7 @@ {{ if .LoggedInUser }}
diff --git a/appview/serververify/verify.go b/appview/serververify/verify.go index 4776f77f..6b47f15c 100644 --- a/appview/serververify/verify.go +++ b/appview/serververify/verify.go @@ -4,12 +4,11 @@ import ( "context" "errors" "fmt" - "io" - "net/http" - "strings" - "time" + indigoxrpc "github.com/bluesky-social/indigo/xrpc" + "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/xrpcclient" "tangled.sh/tangled.sh/core/rbac" ) @@ -24,32 +23,17 @@ func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { scheme = "http" } - url := fmt.Sprintf("%s://%s/owner", scheme, domain) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - - client := &http.Client{ - Timeout: 1 * time.Second, - } - - resp, err := client.Do(req.WithContext(ctx)) - if err != nil || resp.StatusCode != 200 { - return "", fmt.Errorf("failed to fetch /owner") - } - - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data - if err != nil { - return "", fmt.Errorf("failed to read /owner response: %w", err) + host := fmt.Sprintf("%s://%s", scheme, domain) + xrpcc := &indigoxrpc.Client{ + Host: host, } - did := strings.TrimSpace(string(body)) - if did == "" { - return "", fmt.Errorf("empty DID in /owner response") + res, err := tangled.Owner(ctx, xrpcc) + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { + return "", xrpcerr } - return did, nil + return res.Owner, nil } type OwnerMismatch struct { @@ -65,7 +49,7 @@ func (e *OwnerMismatch) Error() string { func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool) error { observedOwner, err := fetchOwner(ctx, domain, dev) if err != nil { - return fmt.Errorf("%w: %w", FetchError, err) + return err } if observedOwner != expectedOwner { diff --git a/appview/spindles/spindles.go b/appview/spindles/spindles.go index 7c5db881..ebd09947 100644 --- a/appview/spindles/spindles.go +++ b/appview/spindles/spindles.go @@ -16,6 +16,7 @@ import ( "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" "tangled.sh/tangled.sh/core/appview/serververify" + "tangled.sh/tangled.sh/core/appview/xrpcclient" "tangled.sh/tangled.sh/core/idresolver" "tangled.sh/tangled.sh/core/rbac" "tangled.sh/tangled.sh/core/tid" @@ -404,8 +405,8 @@ func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { if err != nil { l.Error("verification failed", "err", err) - if errors.Is(err, serververify.FetchError) { - s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") + if errors.Is(err, xrpcclient.ErrXrpcUnsupported) { + s.Pages.Notice(w, noticeId, "Failed to verify spindle, XRPC queries are unsupported on this spindle, consider upgrading!") return } @@ -442,7 +443,7 @@ func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { } w.Header().Set("HX-Reswap", "outerHTML") - s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]}) + s.Pages.SpindleListing(w, pages.SpindleListingParams{Spindle: verifiedSpindle[0]}) } func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { diff --git a/appview/state/router.go b/appview/state/router.go index 9b48ea96..86edfd7d 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -113,6 +113,7 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Get("/", s.HomeOrTimeline) r.Get("/timeline", s.Timeline) + r.With(middleware.AuthMiddleware(s.oauth)).Get("/upgradeBanner", s.UpgradeBanner) r.Route("/repo", func(r chi.Router) { r.Route("/new", func(r chi.Router) { diff --git a/appview/state/state.go b/appview/state/state.go index 418d8d11..3354a3cf 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -228,6 +228,42 @@ func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { }) } +func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetUser(r) + l := s.logger.With("handler", "UpgradeBanner") + l = l.With("did", user.Did) + l = l.With("handle", user.Handle) + + regs, err := db.GetRegistrations( + s.db, + db.FilterEq("did", user.Did), + db.FilterEq("needs_upgrade", 1), + ) + if err != nil { + l.Error("non-fatal: failed to get registrations") + return + } + + spindles, err := db.GetSpindles( + s.db, + db.FilterEq("did", user.Did), + db.FilterEq("needs_upgrade", 1), + ) + if err != nil { + l.Error("non-fatal: failed to get spindles") + return + } + + if regs == nil && spindles == nil { + return + } + + s.pages.UpgradeBanner(w, pages.UpgradeBannerParams{ + Registrations: regs, + Spindles: spindles, + }) +} + func (s *State) Home(w http.ResponseWriter, r *http.Request) { timeline, err := db.MakeTimeline(s.db, 5) if err != nil { @@ -278,7 +314,7 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { for _, k := range pubKeys { key := strings.TrimRight(k.Key, "\n") - w.Write([]byte(fmt.Sprintln(key))) + fmt.Fprintln(w, key) } } diff --git a/knotserver/router.go b/knotserver/router.go index abda8ed1..e17c2c14 100644 --- a/knotserver/router.go +++ b/knotserver/router.go @@ -72,12 +72,6 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j w.Write([]byte("This is a knot server. More info at https://tangled.sh")) }) - owner := func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(h.c.Server.Owner)) - } - // Deprecated: the sh.tangled.knot.owner xrpc call should be used instead - r.Get("/owner", owner) - r.Route("/{did}", func(r chi.Router) { r.Route("/{name}", func(r chi.Router) { // routes for git operations @@ -90,7 +84,6 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j // xrpc apis r.Route("/xrpc", func(r chi.Router) { r.Get("/_health", h.Version) - r.Get("/sh.tangled.knot.owner", owner) r.Mount("/", h.XrpcRouter()) }) diff --git a/knotserver/xrpc/owner.go b/knotserver/xrpc/owner.go new file mode 100644 index 00000000..02730e00 --- /dev/null +++ b/knotserver/xrpc/owner.go @@ -0,0 +1,31 @@ +package xrpc + +import ( + "encoding/json" + "net/http" + + "tangled.sh/tangled.sh/core/api/tangled" + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" +) + +func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { + owner := x.Config.Server.Owner + if owner == "" { + writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) + return + } + + response := tangled.Owner_Output{ + Owner: owner, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + x.Logger.Error("failed to encode response", "error", err) + writeError(w, xrpcerr.NewXrpcError( + xrpcerr.WithTag("InternalServerError"), + xrpcerr.WithMessage("failed to encode response"), + ), http.StatusInternalServerError) + return + } +} diff --git a/knotserver/xrpc/xrpc.go b/knotserver/xrpc/xrpc.go index 594c73ce..aab91164 100644 --- a/knotserver/xrpc/xrpc.go +++ b/knotserver/xrpc/xrpc.go @@ -70,6 +70,9 @@ func (x *Xrpc) Router() http.Handler { // knot query endpoints (no auth required) r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys) + // service query endpoints (no auth required) + r.Get("/"+tangled.OwnerNSID, x.Owner) + return r } diff --git a/spindle/server.go b/spindle/server.go index ca34f169..c2cf7f51 100644 --- a/spindle/server.go +++ b/spindle/server.go @@ -203,9 +203,6 @@ func (s *Spindle) Router() http.Handler { w.Write(motd) }) mux.HandleFunc("/events", s.Events) - mux.HandleFunc("/owner", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(s.cfg.Server.Owner)) - }) mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs) mux.Mount("/xrpc", s.XrpcRouter()) diff --git a/spindle/xrpc/owner.go b/spindle/xrpc/owner.go new file mode 100644 index 00000000..02730e00 --- /dev/null +++ b/spindle/xrpc/owner.go @@ -0,0 +1,31 @@ +package xrpc + +import ( + "encoding/json" + "net/http" + + "tangled.sh/tangled.sh/core/api/tangled" + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" +) + +func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { + owner := x.Config.Server.Owner + if owner == "" { + writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) + return + } + + response := tangled.Owner_Output{ + Owner: owner, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + x.Logger.Error("failed to encode response", "error", err) + writeError(w, xrpcerr.NewXrpcError( + xrpcerr.WithTag("InternalServerError"), + xrpcerr.WithMessage("failed to encode response"), + ), http.StatusInternalServerError) + return + } +} diff --git a/spindle/xrpc/xrpc.go b/spindle/xrpc/xrpc.go index 78f90982..c660da29 100644 --- a/spindle/xrpc/xrpc.go +++ b/spindle/xrpc/xrpc.go @@ -35,9 +35,16 @@ type Xrpc struct { func (x *Xrpc) Router() http.Handler { r := chi.NewRouter() - r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) - r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) - r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) + r.Group(func(r chi.Router) { + r.Use(x.ServiceAuth.VerifyServiceAuth) + + r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) + r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) + r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) + }) + + // service query endpoints (no auth required) + r.Get("/"+tangled.OwnerNSID, x.Owner) return r }