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 }}
+
+ {{range .Registrations}}
+ - {{ .Domain }}
+ {{ end }}
+
+ Repositories hosted on these knots may not be accessible until upgraded.
+ {{ end }}
+
+ {{ if .Spindles }}
+
+ {{range .Spindles}}
+ - {{ .Instance }}
+ {{ end }}
+
+ 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:
-
- {{range $i, $r := .Registrations}}
- - {{ $r.Domain }}
- {{ end }}
-
- 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
}