+31
appview/db/db.go
+31
appview/db/db.go
···
703
703
return err
704
704
})
705
705
706
+
// repurpose the read-only column to "needs-upgrade"
707
+
runMigration(conn, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
708
+
_, err := tx.Exec(`
709
+
alter table registrations rename column read_only to needs_upgrade;
710
+
`)
711
+
return err
712
+
})
713
+
714
+
// require all knots to upgrade after the release of total xrpc
715
+
runMigration(conn, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
716
+
_, err := tx.Exec(`
717
+
update registrations set needs_upgrade = 1;
718
+
`)
719
+
return err
720
+
})
721
+
722
+
// require all knots to upgrade after the release of total xrpc
723
+
runMigration(conn, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
724
+
_, err := tx.Exec(`
725
+
alter table spindles add column needs_upgrade integer not null default 0;
726
+
`)
727
+
if err != nil {
728
+
return err
729
+
}
730
+
731
+
_, err = tx.Exec(`
732
+
update spindles set needs_upgrade = 1;
733
+
`)
734
+
return err
735
+
})
736
+
706
737
return &DB{db}, nil
707
738
}
708
739
+17
-17
appview/db/registration.go
+17
-17
appview/db/registration.go
···
10
10
// Registration represents a knot registration. Knot would've been a better
11
11
// name but we're stuck with this for historical reasons.
12
12
type Registration struct {
13
-
Id int64
14
-
Domain string
15
-
ByDid string
16
-
Created *time.Time
17
-
Registered *time.Time
18
-
ReadOnly bool
13
+
Id int64
14
+
Domain string
15
+
ByDid string
16
+
Created *time.Time
17
+
Registered *time.Time
18
+
NeedsUpgrade bool
19
19
}
20
20
21
21
func (r *Registration) Status() Status {
22
-
if r.ReadOnly {
23
-
return ReadOnly
22
+
if r.NeedsUpgrade {
23
+
return NeedsUpgrade
24
24
} else if r.Registered != nil {
25
25
return Registered
26
26
} else {
···
32
32
return r.Status() == Registered
33
33
}
34
34
35
-
func (r *Registration) IsReadOnly() bool {
36
-
return r.Status() == ReadOnly
35
+
func (r *Registration) IsNeedsUpgrade() bool {
36
+
return r.Status() == NeedsUpgrade
37
37
}
38
38
39
39
func (r *Registration) IsPending() bool {
···
45
45
const (
46
46
Registered Status = iota
47
47
Pending
48
-
ReadOnly
48
+
NeedsUpgrade
49
49
)
50
50
51
51
func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) {
···
64
64
}
65
65
66
66
query := fmt.Sprintf(`
67
-
select id, domain, did, created, registered, read_only
67
+
select id, domain, did, created, registered, needs_upgrade
68
68
from registrations
69
69
%s
70
70
order by created
···
80
80
for rows.Next() {
81
81
var createdAt string
82
82
var registeredAt sql.Null[string]
83
-
var readOnly int
83
+
var needsUpgrade int
84
84
var reg Registration
85
85
86
-
err = rows.Scan(®.Id, ®.Domain, ®.ByDid, &createdAt, ®isteredAt, &readOnly)
86
+
err = rows.Scan(®.Id, ®.Domain, ®.ByDid, &createdAt, ®isteredAt, &needsUpgrade)
87
87
if err != nil {
88
88
return nil, err
89
89
}
···
98
98
}
99
99
}
100
100
101
-
if readOnly != 0 {
102
-
reg.ReadOnly = true
101
+
if needsUpgrade != 0 {
102
+
reg.NeedsUpgrade = true
103
103
}
104
104
105
105
registrations = append(registrations, reg)
···
116
116
args = append(args, filter.Arg()...)
117
117
}
118
118
119
-
query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0"
119
+
query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), needs_upgrade = 0"
120
120
if len(conditions) > 0 {
121
121
query += " where " + strings.Join(conditions, " and ")
122
122
}
+14
-7
appview/db/spindle.go
+14
-7
appview/db/spindle.go
···
10
10
)
11
11
12
12
type Spindle struct {
13
-
Id int
14
-
Owner syntax.DID
15
-
Instance string
16
-
Verified *time.Time
17
-
Created time.Time
13
+
Id int
14
+
Owner syntax.DID
15
+
Instance string
16
+
Verified *time.Time
17
+
Created time.Time
18
+
NeedsUpgrade bool
18
19
}
19
20
20
21
type SpindleMember struct {
···
42
43
}
43
44
44
45
query := fmt.Sprintf(
45
-
`select id, owner, instance, verified, created
46
+
`select id, owner, instance, verified, created, needs_upgrade
46
47
from spindles
47
48
%s
48
49
order by created
···
61
62
var spindle Spindle
62
63
var createdAt string
63
64
var verified sql.NullString
65
+
var needsUpgrade int
64
66
65
67
if err := rows.Scan(
66
68
&spindle.Id,
···
68
70
&spindle.Instance,
69
71
&verified,
70
72
&createdAt,
73
+
&needsUpgrade,
71
74
); err != nil {
72
75
return nil, err
73
76
}
···
86
89
spindle.Verified = &t
87
90
}
88
91
92
+
if needsUpgrade != 0 {
93
+
spindle.NeedsUpgrade = true
94
+
}
95
+
89
96
spindles = append(spindles, spindle)
90
97
}
91
98
···
115
122
whereClause = " where " + strings.Join(conditions, " and ")
116
123
}
117
124
118
-
query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
125
+
query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now'), needs_upgrade = 0 %s`, whereClause)
119
126
120
127
res, err := e.Exec(query, args...)
121
128
if err != nil {
+5
-57
appview/knots/knots.go
+5
-57
appview/knots/knots.go
···
3
3
import (
4
4
"errors"
5
5
"fmt"
6
-
"log"
7
6
"log/slog"
8
7
"net/http"
9
8
"slices"
···
17
16
"tangled.sh/tangled.sh/core/appview/oauth"
18
17
"tangled.sh/tangled.sh/core/appview/pages"
19
18
"tangled.sh/tangled.sh/core/appview/serververify"
19
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
20
20
"tangled.sh/tangled.sh/core/eventconsumer"
21
21
"tangled.sh/tangled.sh/core/idresolver"
22
22
"tangled.sh/tangled.sh/core/rbac"
···
49
49
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
50
50
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
51
51
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
52
-
53
-
r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner)
54
52
55
53
return r
56
54
}
···
399
397
if err != nil {
400
398
l.Error("verification failed", "err", err)
401
399
402
-
if errors.Is(err, serververify.FetchError) {
403
-
k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
400
+
if errors.Is(err, xrpcclient.ErrXrpcUnsupported) {
401
+
k.Pages.Notice(w, noticeId, "Failed to verify knot, XRPC queries are unsupported on this knot, consider upgrading!")
404
402
return
405
403
}
406
404
···
420
418
return
421
419
}
422
420
423
-
// if this knot was previously read-only, then emit a record too
421
+
// if this knot requires upgrade, then emit a record too
424
422
//
425
423
// this is part of migrating from the old knot system to the new one
426
-
if registration.ReadOnly {
424
+
if registration.NeedsUpgrade {
427
425
// re-announce by registering under same rkey
428
426
client, err := k.OAuth.AuthorizedClient(r)
429
427
if err != nil {
···
484
482
return
485
483
}
486
484
updatedRegistration := registrations[0]
487
-
488
-
log.Println(updatedRegistration)
489
485
490
486
w.Header().Set("HX-Reswap", "outerHTML")
491
487
k.Pages.KnotListing(w, pages.KnotListingParams{
···
678
674
// ok
679
675
k.Pages.HxRefresh(w)
680
676
}
681
-
682
-
func (k *Knots) banner(w http.ResponseWriter, r *http.Request) {
683
-
user := k.OAuth.GetUser(r)
684
-
l := k.Logger.With("handler", "banner")
685
-
l = l.With("did", user.Did)
686
-
l = l.With("handle", user.Handle)
687
-
688
-
allRegistrations, err := db.GetRegistrations(
689
-
k.Db,
690
-
db.FilterEq("did", user.Did),
691
-
)
692
-
if err != nil {
693
-
l.Error("non-fatal: failed to get registrations")
694
-
return
695
-
}
696
-
697
-
httpClient := &http.Client{Timeout: 5 * time.Second}
698
-
regs404 := []db.Registration{}
699
-
for _, reg := range allRegistrations {
700
-
healthURL := fmt.Sprintf("http://%s/xrpc/_health", reg.Domain)
701
-
702
-
fmt.Println(healthURL)
703
-
704
-
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, healthURL, nil)
705
-
if err != nil {
706
-
l.Error("failed to create health check request", "domain", reg.Domain, "err", err)
707
-
continue
708
-
}
709
-
710
-
resp, err := httpClient.Do(req)
711
-
if err != nil {
712
-
l.Error("failed to make health check request", "domain", reg.Domain, "err", err)
713
-
continue
714
-
}
715
-
defer resp.Body.Close()
716
-
717
-
if resp.StatusCode == http.StatusNotFound {
718
-
regs404 = append(regs404, reg)
719
-
}
720
-
}
721
-
if len(regs404) == 0 {
722
-
return
723
-
}
724
-
725
-
k.Pages.KnotBanner(w, pages.KnotBannerParams{
726
-
Registrations: regs404,
727
-
})
728
-
}
+4
-3
appview/pages/pages.go
+4
-3
appview/pages/pages.go
···
351
351
return p.execute("user/settings/emails", w, params)
352
352
}
353
353
354
-
type KnotBannerParams struct {
354
+
type UpgradeBannerParams struct {
355
355
Registrations []db.Registration
356
+
Spindles []db.Spindle
356
357
}
357
358
358
-
func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error {
359
-
return p.executePlain("knots/fragments/bannerRequiresUpgrade", w, params)
359
+
func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error {
360
+
return p.executePlain("banner", w, params)
360
361
}
361
362
362
363
type KnotsParams struct {
+2
-2
appview/pages/templates/knots/fragments/knotListing.html
+2
-2
appview/pages/templates/knots/fragments/knotListing.html
···
36
36
</span>
37
37
{{ template "knots/fragments/addMemberModal" . }}
38
38
{{ block "knotDeleteButton" . }} {{ end }}
39
-
{{ else if .IsReadOnly }}
39
+
{{ else if .IsNeedsUpgrade }}
40
40
<span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}">
41
-
{{ i "shield-alert" "w-4 h-4" }} read-only
41
+
{{ i "shield-alert" "w-4 h-4" }} needs upgrade
42
42
</span>
43
43
{{ block "knotRetryButton" . }} {{ end }}
44
44
{{ block "knotDeleteButton" . }} {{ end }}
+1
-1
appview/pages/templates/layouts/base.html
+1
-1
appview/pages/templates/layouts/base.html
···
53
53
{{ if .LoggedInUser }}
54
54
<div id="upgrade-banner"
55
55
class="z-50 fixed bottom-0 left-0 right-0 w-full flex justify-center bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm"
56
-
hx-get="/knots/upgradeBanner"
56
+
hx-get="/banner"
57
57
hx-trigger="load"
58
58
hx-swap="innerHTML">
59
59
</div>
+11
-27
appview/serververify/verify.go
+11
-27
appview/serververify/verify.go
···
4
4
"context"
5
5
"errors"
6
6
"fmt"
7
-
"io"
8
-
"net/http"
9
-
"strings"
10
-
"time"
11
7
8
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
9
+
"tangled.sh/tangled.sh/core/api/tangled"
12
10
"tangled.sh/tangled.sh/core/appview/db"
11
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
13
12
"tangled.sh/tangled.sh/core/rbac"
14
13
)
15
14
···
24
23
scheme = "http"
25
24
}
26
25
27
-
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
28
-
req, err := http.NewRequest("GET", url, nil)
29
-
if err != nil {
30
-
return "", err
31
-
}
32
-
33
-
client := &http.Client{
34
-
Timeout: 1 * time.Second,
35
-
}
36
-
37
-
resp, err := client.Do(req.WithContext(ctx))
38
-
if err != nil || resp.StatusCode != 200 {
39
-
return "", fmt.Errorf("failed to fetch /owner")
26
+
host := fmt.Sprintf("%s://%s", scheme, domain)
27
+
xrpcc := &indigoxrpc.Client{
28
+
Host: host,
40
29
}
41
30
42
-
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
43
-
if err != nil {
44
-
return "", fmt.Errorf("failed to read /owner response: %w", err)
31
+
res, err := tangled.Owner(ctx, xrpcc)
32
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
33
+
return "", xrpcerr
45
34
}
46
35
47
-
did := strings.TrimSpace(string(body))
48
-
if did == "" {
49
-
return "", fmt.Errorf("empty DID in /owner response")
50
-
}
51
-
52
-
return did, nil
36
+
return res.Owner, nil
53
37
}
54
38
55
39
type OwnerMismatch struct {
···
65
49
func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool) error {
66
50
observedOwner, err := fetchOwner(ctx, domain, dev)
67
51
if err != nil {
68
-
return fmt.Errorf("%w: %w", FetchError, err)
52
+
return err
69
53
}
70
54
71
55
if observedOwner != expectedOwner {
+4
-3
appview/spindles/spindles.go
+4
-3
appview/spindles/spindles.go
···
16
16
"tangled.sh/tangled.sh/core/appview/oauth"
17
17
"tangled.sh/tangled.sh/core/appview/pages"
18
18
"tangled.sh/tangled.sh/core/appview/serververify"
19
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
19
20
"tangled.sh/tangled.sh/core/idresolver"
20
21
"tangled.sh/tangled.sh/core/rbac"
21
22
"tangled.sh/tangled.sh/core/tid"
···
404
405
if err != nil {
405
406
l.Error("verification failed", "err", err)
406
407
407
-
if errors.Is(err, serververify.FetchError) {
408
-
s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
408
+
if errors.Is(err, xrpcclient.ErrXrpcUnsupported) {
409
+
s.Pages.Notice(w, noticeId, "Failed to verify spindle, XRPC queries are unsupported on this spindle, consider upgrading!")
409
410
return
410
411
}
411
412
···
442
443
}
443
444
444
445
w.Header().Set("HX-Reswap", "outerHTML")
445
-
s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]})
446
+
s.Pages.SpindleListing(w, pages.SpindleListingParams{Spindle: verifiedSpindle[0]})
446
447
}
447
448
448
449
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
+1
appview/state/router.go
+37
-1
appview/state/state.go
+37
-1
appview/state/state.go
···
228
228
})
229
229
}
230
230
231
+
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
232
+
user := s.oauth.GetUser(r)
233
+
l := s.logger.With("handler", "UpgradeBanner")
234
+
l = l.With("did", user.Did)
235
+
l = l.With("handle", user.Handle)
236
+
237
+
regs, err := db.GetRegistrations(
238
+
s.db,
239
+
db.FilterEq("did", user.Did),
240
+
db.FilterEq("needs_upgrade", 1),
241
+
)
242
+
if err != nil {
243
+
l.Error("non-fatal: failed to get registrations")
244
+
return
245
+
}
246
+
247
+
spindles, err := db.GetSpindles(
248
+
s.db,
249
+
db.FilterEq("did", user.Did),
250
+
db.FilterEq("needs_upgrade", 1),
251
+
)
252
+
if err != nil {
253
+
l.Error("non-fatal: failed to get spindles")
254
+
return
255
+
}
256
+
257
+
if regs == nil && spindles == nil {
258
+
return
259
+
}
260
+
261
+
s.pages.UpgradeBanner(w, pages.UpgradeBannerParams{
262
+
Registrations: regs,
263
+
Spindles: spindles,
264
+
})
265
+
}
266
+
231
267
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
232
268
timeline, err := db.MakeTimeline(s.db, 5)
233
269
if err != nil {
···
278
314
279
315
for _, k := range pubKeys {
280
316
key := strings.TrimRight(k.Key, "\n")
281
-
w.Write([]byte(fmt.Sprintln(key)))
317
+
fmt.Fprintln(w, key)
282
318
}
283
319
}
284
320
-7
knotserver/router.go
-7
knotserver/router.go
···
72
72
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
73
73
})
74
74
75
-
owner := func(w http.ResponseWriter, r *http.Request) {
76
-
w.Write([]byte(h.c.Server.Owner))
77
-
}
78
-
// Deprecated: the sh.tangled.knot.owner xrpc call should be used instead
79
-
r.Get("/owner", owner)
80
-
81
75
r.Route("/{did}", func(r chi.Router) {
82
76
r.Route("/{name}", func(r chi.Router) {
83
77
// routes for git operations
···
90
84
// xrpc apis
91
85
r.Route("/xrpc", func(r chi.Router) {
92
86
r.Get("/_health", h.Version)
93
-
r.Get("/sh.tangled.knot.owner", owner)
94
87
r.Mount("/", h.XrpcRouter())
95
88
})
96
89
+31
knotserver/xrpc/owner.go
+31
knotserver/xrpc/owner.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
7
+
"tangled.sh/tangled.sh/core/api/tangled"
8
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
9
+
)
10
+
11
+
func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) {
12
+
owner := x.Config.Server.Owner
13
+
if owner == "" {
14
+
writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError)
15
+
return
16
+
}
17
+
18
+
response := tangled.Owner_Output{
19
+
Owner: owner,
20
+
}
21
+
22
+
w.Header().Set("Content-Type", "application/json")
23
+
if err := json.NewEncoder(w).Encode(response); err != nil {
24
+
x.Logger.Error("failed to encode response", "error", err)
25
+
writeError(w, xrpcerr.NewXrpcError(
26
+
xrpcerr.WithTag("InternalServerError"),
27
+
xrpcerr.WithMessage("failed to encode response"),
28
+
), http.StatusInternalServerError)
29
+
return
30
+
}
31
+
}
+3
knotserver/xrpc/xrpc.go
+3
knotserver/xrpc/xrpc.go
-3
spindle/server.go
-3
spindle/server.go
···
203
203
w.Write(motd)
204
204
})
205
205
mux.HandleFunc("/events", s.Events)
206
-
mux.HandleFunc("/owner", func(w http.ResponseWriter, r *http.Request) {
207
-
w.Write([]byte(s.cfg.Server.Owner))
208
-
})
209
206
mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
210
207
211
208
mux.Mount("/xrpc", s.XrpcRouter())
+31
spindle/xrpc/owner.go
+31
spindle/xrpc/owner.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
7
+
"tangled.sh/tangled.sh/core/api/tangled"
8
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
9
+
)
10
+
11
+
func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) {
12
+
owner := x.Config.Server.Owner
13
+
if owner == "" {
14
+
writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError)
15
+
return
16
+
}
17
+
18
+
response := tangled.Owner_Output{
19
+
Owner: owner,
20
+
}
21
+
22
+
w.Header().Set("Content-Type", "application/json")
23
+
if err := json.NewEncoder(w).Encode(response); err != nil {
24
+
x.Logger.Error("failed to encode response", "error", err)
25
+
writeError(w, xrpcerr.NewXrpcError(
26
+
xrpcerr.WithTag("InternalServerError"),
27
+
xrpcerr.WithMessage("failed to encode response"),
28
+
), http.StatusInternalServerError)
29
+
return
30
+
}
31
+
}
+10
-3
spindle/xrpc/xrpc.go
+10
-3
spindle/xrpc/xrpc.go
···
35
35
func (x *Xrpc) Router() http.Handler {
36
36
r := chi.NewRouter()
37
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)
38
+
r.Group(func(r chi.Router) {
39
+
r.Use(x.ServiceAuth.VerifyServiceAuth)
40
+
41
+
r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
42
+
r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
43
+
r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
44
+
})
45
+
46
+
// service query endpoints (no auth required)
47
+
r.Get("/"+tangled.OwnerNSID, x.Owner)
41
48
42
49
return r
43
50
}