+16
appview/config/config.go
+16
appview/config/config.go
···
28
28
Jwks string `env:"JWKS"`
29
29
}
30
30
31
+
type PlcConfig struct {
32
+
PLCURL string `env:"URL, default=https://plc.directory"`
33
+
}
34
+
31
35
type JetstreamConfig struct {
32
36
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
33
37
}
···
78
82
TurnstileSecretKey string `env:"TURNSTILE_SECRET_KEY"`
79
83
}
80
84
85
+
type LabelConfig struct {
86
+
DefaultLabelDefs []string `env:"DEFAULTS"` // delimiter=,
87
+
GoodFirstIssue string `env:"GFI, default=at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"`
88
+
}
89
+
81
90
func (cfg RedisConfig) ToURL() string {
82
91
u := &url.URL{
83
92
Scheme: "redis",
···
103
112
Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"`
104
113
OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"`
105
114
Redis RedisConfig `env:",prefix=TANGLED_REDIS_"`
115
+
Plc PlcConfig `env:",prefix=TANGLED_PLC_"`
106
116
Pds PdsConfig `env:",prefix=TANGLED_PDS_"`
107
117
Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"`
118
+
Label LabelConfig `env:",prefix=TANGLED_LABEL_"`
108
119
}
109
120
110
121
func LoadConfig(ctx context.Context) (*Config, error) {
···
112
123
err := envconfig.Process(ctx, &cfg)
113
124
if err != nil {
114
125
return nil, err
126
+
}
127
+
128
+
fmt.Println("default labels:")
129
+
for _, l := range cfg.Label.DefaultLabelDefs {
130
+
fmt.Println(l)
115
131
}
116
132
117
133
return &cfg, nil
+25
-43
appview/models/label.go
+25
-43
appview/models/label.go
···
14
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
15
"github.com/bluesky-social/indigo/xrpc"
16
16
"tangled.org/core/api/tangled"
17
-
"tangled.org/core/consts"
18
17
"tangled.org/core/idresolver"
19
18
)
20
19
···
461
460
return result
462
461
}
463
462
464
-
var (
465
-
LabelWontfix = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "wontfix")
466
-
LabelDuplicate = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "duplicate")
467
-
LabelAssignee = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "assignee")
468
-
LabelGoodFirstIssue = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
469
-
LabelDocumentation = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "documentation")
470
-
)
463
+
func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
464
+
var labelDefs []LabelDefinition
465
+
ctx := context.Background()
471
466
472
-
func DefaultLabelDefs() []string {
473
-
return []string{
474
-
LabelWontfix,
475
-
LabelDuplicate,
476
-
LabelAssignee,
477
-
LabelGoodFirstIssue,
478
-
LabelDocumentation,
479
-
}
480
-
}
467
+
for _, dl := range aturis {
468
+
atUri, err := syntax.ParseATURI(dl)
469
+
if err != nil {
470
+
return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
471
+
}
472
+
if atUri.Collection() != tangled.LabelDefinitionNSID {
473
+
return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
474
+
}
481
475
482
-
func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) {
483
-
resolved, err := r.ResolveIdent(context.Background(), consts.TangledDid)
484
-
if err != nil {
485
-
return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", consts.TangledDid, err)
486
-
}
487
-
pdsEndpoint := resolved.PDSEndpoint()
488
-
if pdsEndpoint == "" {
489
-
return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", consts.TangledDid)
490
-
}
491
-
client := &xrpc.Client{
492
-
Host: pdsEndpoint,
493
-
}
476
+
owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
477
+
if err != nil {
478
+
return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
479
+
}
494
480
495
-
var labelDefs []LabelDefinition
481
+
xrpcc := xrpc.Client{
482
+
Host: owner.PDSEndpoint(),
483
+
}
496
484
497
-
for _, dl := range DefaultLabelDefs() {
498
-
atUri := syntax.ATURI(dl)
499
-
parsedUri, err := syntax.ParseATURI(string(atUri))
500
-
if err != nil {
501
-
return nil, fmt.Errorf("failed to parse AT-URI %s: %v", atUri, err)
502
-
}
503
485
record, err := atproto.RepoGetRecord(
504
-
context.Background(),
505
-
client,
486
+
ctx,
487
+
&xrpcc,
506
488
"",
507
-
parsedUri.Collection().String(),
508
-
parsedUri.Authority().String(),
509
-
parsedUri.RecordKey().String(),
489
+
atUri.Collection().String(),
490
+
atUri.Authority().String(),
491
+
atUri.RecordKey().String(),
510
492
)
511
493
if err != nil {
512
494
return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
···
526
508
}
527
509
528
510
labelDef, err := LabelDefinitionFromRecord(
529
-
parsedUri.Authority().String(),
530
-
parsedUri.RecordKey().String(),
511
+
atUri.Authority().String(),
512
+
atUri.RecordKey().String(),
531
513
labelRecord,
532
514
)
533
515
if err != nil {
+9
-1
appview/oauth/oauth.go
+9
-1
appview/oauth/oauth.go
···
58
58
59
59
sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret))
60
60
61
+
clientApp := oauth.NewClientApp(&oauthConfig, authStore)
62
+
// use same plc directory from idresolver
63
+
clientApp.Dir = res.Directory()
64
+
// allow non-public transports in dev mode
65
+
if config.Core.Dev {
66
+
clientApp.Resolver.Client.Transport = http.DefaultTransport
67
+
}
68
+
61
69
return &OAuth{
62
-
ClientApp: oauth.NewClientApp(&oauthConfig, authStore),
70
+
ClientApp: clientApp,
63
71
Config: config,
64
72
SessStore: sessStore,
65
73
JwksUri: jwksUri,
+2
-3
appview/pages/funcmap.go
+2
-3
appview/pages/funcmap.go
···
297
297
},
298
298
299
299
"normalizeForHtmlId": func(s string) string {
300
-
normalized := strings.ReplaceAll(s, ":", "_")
301
-
normalized = strings.ReplaceAll(normalized, ".", "_")
302
-
return normalized
300
+
// TODO: extend this to handle other cases?
301
+
return strings.ReplaceAll(s, ":", "_")
303
302
},
304
303
"sshFingerprint": func(pubKey string) string {
305
304
fp, err := crypto.SSHFingerprint(pubKey)
+2
-2
appview/repo/repo.go
+2
-2
appview/repo/repo.go
···
1974
1974
return
1975
1975
}
1976
1976
1977
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs()))
1977
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
1978
1978
if err != nil {
1979
1979
l.Error("failed to fetch labels", "err", err)
1980
1980
rp.pages.Error503(w)
···
2253
2253
Source: sourceAt,
2254
2254
Description: f.Repo.Description,
2255
2255
Created: time.Now(),
2256
-
Labels: models.DefaultLabelDefs(),
2256
+
Labels: rp.config.Label.DefaultLabelDefs,
2257
2257
}
2258
2258
record := repo.AsRecord()
2259
2259
-1
appview/state/follow.go
-1
appview/state/follow.go
+10
-4
appview/state/gfi.go
+10
-4
appview/state/gfi.go
···
1
1
package state
2
2
3
3
import (
4
-
"fmt"
5
4
"log"
6
5
"net/http"
7
6
"sort"
8
7
9
8
"github.com/bluesky-social/indigo/atproto/syntax"
10
-
"tangled.org/core/api/tangled"
11
9
"tangled.org/core/appview/db"
12
10
"tangled.org/core/appview/models"
13
11
"tangled.org/core/appview/pages"
···
23
21
page = pagination.FirstPage()
24
22
}
25
23
26
-
goodFirstIssueLabel := fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
24
+
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
25
+
26
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
27
+
if err != nil {
28
+
log.Println("failed to get gfi label def", err)
29
+
s.pages.Error503(w)
30
+
return
31
+
}
27
32
28
33
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
29
34
if err != nil {
···
38
43
RepoGroups: []*models.RepoGroup{},
39
44
LabelDefs: make(map[string]*models.LabelDefinition),
40
45
Page: page,
46
+
GfiLabel: gfiLabelDef,
41
47
})
42
48
return
43
49
}
···
146
152
RepoGroups: paginatedGroups,
147
153
LabelDefs: labelDefsMap,
148
154
Page: page,
149
-
GfiLabel: labelDefsMap[goodFirstIssueLabel],
155
+
GfiLabel: gfiLabelDef,
150
156
})
151
157
}
+1
appview/state/login.go
+1
appview/state/login.go
+7
-8
appview/state/state.go
+7
-8
appview/state/state.go
···
70
70
return nil, fmt.Errorf("failed to create enforcer: %w", err)
71
71
}
72
72
73
-
res, err := idresolver.RedisResolver(config.Redis.ToURL())
73
+
res, err := idresolver.RedisResolver(config.Redis.ToURL(), config.Plc.PLCURL)
74
74
if err != nil {
75
75
logger.Error("failed to create redis resolver", "err", err)
76
-
res = idresolver.DefaultResolver()
76
+
res = idresolver.DefaultResolver(config.Plc.PLCURL)
77
77
}
78
78
79
79
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
···
121
121
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
122
122
}
123
123
124
-
if err := BackfillDefaultDefs(d, res); err != nil {
124
+
if err := BackfillDefaultDefs(d, res, config.Label.DefaultLabelDefs); err != nil {
125
125
return nil, fmt.Errorf("failed to backfill default label defs: %w", err)
126
126
}
127
127
···
284
284
return
285
285
}
286
286
287
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", models.LabelGoodFirstIssue))
287
+
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
288
288
if err != nil {
289
289
// non-fatal
290
290
}
···
506
506
Rkey: rkey,
507
507
Description: description,
508
508
Created: time.Now(),
509
-
Labels: models.DefaultLabelDefs(),
509
+
Labels: s.config.Label.DefaultLabelDefs,
510
510
}
511
511
record := repo.AsRecord()
512
512
···
648
648
return err
649
649
}
650
650
651
-
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver) error {
652
-
defaults := models.DefaultLabelDefs()
651
+
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
653
652
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
654
653
if err != nil {
655
654
return err
···
659
658
return nil
660
659
}
661
660
662
-
labelDefs, err := models.FetchDefaultDefs(r)
661
+
labelDefs, err := models.FetchLabelDefs(r, defaults)
663
662
if err != nil {
664
663
return err
665
664
}
+9
-3
guard/guard.go
+9
-3
guard/guard.go
···
16
16
securejoin "github.com/cyphar/filepath-securejoin"
17
17
"github.com/urfave/cli/v3"
18
18
"tangled.org/core/idresolver"
19
+
"tangled.org/core/knotserver/config"
19
20
"tangled.org/core/log"
20
21
)
21
22
···
56
57
57
58
func Run(ctx context.Context, cmd *cli.Command) error {
58
59
l := log.FromContext(ctx)
60
+
61
+
c, err := config.Load(ctx)
62
+
if err != nil {
63
+
return fmt.Errorf("failed to load config: %w", err)
64
+
}
59
65
60
66
incomingUser := cmd.String("user")
61
67
gitDir := cmd.String("git-dir")
···
122
128
}
123
129
124
130
didOrHandle := components[0]
125
-
identity := resolveIdentity(ctx, l, didOrHandle)
131
+
identity := resolveIdentity(ctx, c, l, didOrHandle)
126
132
did := identity.DID.String()
127
133
repoName := components[1]
128
134
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
···
195
201
return nil
196
202
}
197
203
198
-
func resolveIdentity(ctx context.Context, l *slog.Logger, didOrHandle string) *identity.Identity {
199
-
resolver := idresolver.DefaultResolver()
204
+
func resolveIdentity(ctx context.Context, c *config.Config, l *slog.Logger, didOrHandle string) *identity.Identity {
205
+
resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
200
206
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
201
207
if err != nil {
202
208
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
+17
-8
idresolver/resolver.go
+17
-8
idresolver/resolver.go
···
17
17
directory identity.Directory
18
18
}
19
19
20
-
func BaseDirectory() identity.Directory {
20
+
func BaseDirectory(plcUrl string) identity.Directory {
21
21
base := identity.BaseDirectory{
22
-
PLCURL: identity.DefaultPLCURL,
22
+
PLCURL: plcUrl,
23
23
HTTPClient: http.Client{
24
24
Timeout: time.Second * 10,
25
25
Transport: &http.Transport{
···
42
42
return &base
43
43
}
44
44
45
-
func RedisDirectory(url string) (identity.Directory, error) {
45
+
func RedisDirectory(url, plcUrl string) (identity.Directory, error) {
46
46
hitTTL := time.Hour * 24
47
47
errTTL := time.Second * 30
48
48
invalidHandleTTL := time.Minute * 5
49
-
return redisdir.NewRedisDirectory(BaseDirectory(), url, hitTTL, errTTL, invalidHandleTTL, 10000)
49
+
return redisdir.NewRedisDirectory(
50
+
BaseDirectory(plcUrl),
51
+
url,
52
+
hitTTL,
53
+
errTTL,
54
+
invalidHandleTTL,
55
+
10000,
56
+
)
50
57
}
51
58
52
-
func DefaultResolver() *Resolver {
59
+
func DefaultResolver(plcUrl string) *Resolver {
60
+
base := BaseDirectory(plcUrl)
61
+
cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5)
53
62
return &Resolver{
54
-
directory: identity.DefaultDirectory(),
63
+
directory: &cached,
55
64
}
56
65
}
57
66
58
-
func RedisResolver(redisUrl string) (*Resolver, error) {
59
-
directory, err := RedisDirectory(redisUrl)
67
+
func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) {
68
+
directory, err := RedisDirectory(redisUrl, plcUrl)
60
69
if err != nil {
61
70
return nil, err
62
71
}
+1
knotserver/config/config.go
+1
knotserver/config/config.go
···
19
19
InternalListenAddr string `env:"INTERNAL_LISTEN_ADDR, default=127.0.0.1:5444"`
20
20
DBPath string `env:"DB_PATH, default=knotserver.db"`
21
21
Hostname string `env:"HOSTNAME, required"`
22
+
PlcUrl string `env:"PLC_URL, default=plc.directory"`
22
23
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
23
24
Owner string `env:"OWNER, required"`
24
25
LogDids bool `env:"LOG_DIDS, default=true"`
+3
-7
knotserver/ingester.go
+3
-7
knotserver/ingester.go
···
16
16
"github.com/bluesky-social/jetstream/pkg/models"
17
17
securejoin "github.com/cyphar/filepath-securejoin"
18
18
"tangled.org/core/api/tangled"
19
-
"tangled.org/core/idresolver"
20
19
"tangled.org/core/knotserver/db"
21
20
"tangled.org/core/knotserver/git"
22
21
"tangled.org/core/log"
···
120
119
}
121
120
122
121
// resolve this aturi to extract the repo record
123
-
resolver := idresolver.DefaultResolver()
124
-
ident, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
122
+
ident, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String())
125
123
if err != nil || ident.Handle.IsInvalidHandle() {
126
124
return fmt.Errorf("failed to resolve handle: %w", err)
127
125
}
···
233
231
return err
234
232
}
235
233
236
-
resolver := idresolver.DefaultResolver()
237
-
238
-
subjectId, err := resolver.ResolveIdent(ctx, record.Subject)
234
+
subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject)
239
235
if err != nil || subjectId.Handle.IsInvalidHandle() {
240
236
return err
241
237
}
242
238
243
239
// TODO: fix this for good, we need to fetch the record here unfortunately
244
240
// resolve this aturi to extract the repo record
245
-
owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
241
+
owner, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String())
246
242
if err != nil || owner.Handle.IsInvalidHandle() {
247
243
return fmt.Errorf("failed to resolve handle: %w", err)
248
244
}
+1
-1
knotserver/internal.go
+1
-1
knotserver/internal.go
···
145
145
146
146
func (h *InternalHandle) replyCompare(line git.PostReceiveLine, repoOwner string, gitRelativeDir string, repoName string, ctx context.Context) ([]string, error) {
147
147
l := h.l.With("handler", "replyCompare")
148
-
userIdent, err := idresolver.DefaultResolver().ResolveIdent(ctx, repoOwner)
148
+
userIdent, err := idresolver.DefaultResolver(h.c.Server.PlcUrl).ResolveIdent(ctx, repoOwner)
149
149
user := repoOwner
150
150
if err != nil {
151
151
l.Error("Failed to fetch user identity", "err", err)
+1
-1
knotserver/router.go
+1
-1
knotserver/router.go
+45
local-infra/Caddyfile
+45
local-infra/Caddyfile
···
1
+
{
2
+
storage file_system /data/
3
+
debug
4
+
pki {
5
+
ca localtangled {
6
+
name "LocalTangledCA"
7
+
}
8
+
}
9
+
auto_https disable_redirects
10
+
}
11
+
12
+
plc.tngl.boltless.dev {
13
+
tls {
14
+
issuer internal {
15
+
ca localtangled
16
+
}
17
+
}
18
+
reverse_proxy http://plc:8080
19
+
}
20
+
21
+
*.pds.tngl.boltless.dev, pds.tngl.boltless.dev {
22
+
tls {
23
+
issuer internal {
24
+
ca localtangled
25
+
}
26
+
}
27
+
reverse_proxy http://pds:3000
28
+
}
29
+
30
+
jetstream.tngl.boltless.dev {
31
+
tls {
32
+
issuer internal {
33
+
ca localtangled
34
+
}
35
+
}
36
+
reverse_proxy http://jetstream:6008
37
+
}
38
+
39
+
http://knot.tngl.boltless.dev {
40
+
reverse_proxy http://host.docker.internal:6000
41
+
}
42
+
43
+
http://spindle.tngl.boltless.dev {
44
+
reverse_proxy http://host.docker.internal:6555
45
+
}
+12
local-infra/cert/localtangled/intermediate.crt
+12
local-infra/cert/localtangled/intermediate.crt
···
1
+
-----BEGIN CERTIFICATE-----
2
+
MIIBuTCCAWCgAwIBAgIRALKb0dndMd7jlCHAzm0G+N4wCgYIKoZIzj0EAwIwKTEn
3
+
MCUGA1UEAxMeTG9jYWxUYW5nbGVkQ0EgLSAyMDI1IEVDQyBSb290MB4XDTI1MTAy
4
+
MTA3NDAwNloXDTI1MTAyODA3NDAwNlowLDEqMCgGA1UEAxMhTG9jYWxUYW5nbGVk
5
+
Q0EgLSBFQ0MgSW50ZXJtZWRpYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
6
+
bX+zyr9rLxF3E8oCZwJluCKX/xmU4waabkjaTGbI5K0cemiAAmZRJ2lVhgh+KfXD
7
+
PpTmt+YE6FUF4xAWADOUuaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI
8
+
MAYBAf8CAQAwHQYDVR0OBBYEFIoGsfx3Qg/9qG7tm7CZ1pHYl3prMB8GA1UdIwQY
9
+
MBaAFCkl8dPP2IAMTPru6WEHLP1hySEQMAoGCCqGSM49BAMCA0cAMEQCIFc3gOEl
10
+
aUR/OWbQuWvYwoTZs81ERj73ZeQWy4a3i4ooAiAB7Mnih/7kEvLyfkjLRgRXrtlq
11
+
kVmXVyWHIncR6Bsktw==
12
+
-----END CERTIFICATE-----
+5
local-infra/cert/localtangled/intermediate.key
+5
local-infra/cert/localtangled/intermediate.key
+11
local-infra/cert/localtangled/root.crt
+11
local-infra/cert/localtangled/root.crt
···
1
+
-----BEGIN CERTIFICATE-----
2
+
MIIBlTCCATygAwIBAgIRAMDTcwNxYDMgtUNC5LkCeEQwCgYIKoZIzj0EAwIwKTEn
3
+
MCUGA1UEAxMeTG9jYWxUYW5nbGVkQ0EgLSAyMDI1IEVDQyBSb290MB4XDTI1MTAx
4
+
NzE2MTE0NVoXDTM1MDgyNjE2MTE0NVowKTEnMCUGA1UEAxMeTG9jYWxUYW5nbGVk
5
+
Q0EgLSAyMDI1IEVDQyBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7rFM
6
+
4oNfT0UMqMuc3L60TCLeTd58WFSUYnKl7R1HOHDWeWZhhoNdWguXJSHhFPiWmQ5E
7
+
+fiI7KvDAVQGHzfUAqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB
8
+
Af8CAQEwHQYDVR0OBBYEFCkl8dPP2IAMTPru6WEHLP1hySEQMAoGCCqGSM49BAMC
9
+
A0cAMEQCIFjSGjvie1gO/JuNtP2HqeUHQNEh82K1fXdks54up3KEAiBWQDaOYeZ2
10
+
zVTiKe8ZQHpH3glXsIS0USsxeKaohMp0zA==
11
+
-----END CERTIFICATE-----
+5
local-infra/cert/localtangled/root.key
+5
local-infra/cert/localtangled/root.key
+82
local-infra/docker-compose.yml
+82
local-infra/docker-compose.yml
···
1
+
name: tangled-local-infra
2
+
services:
3
+
caddy:
4
+
container_name: caddy
5
+
image: caddy:2
6
+
depends_on:
7
+
- pds
8
+
restart: unless-stopped
9
+
cap_add:
10
+
- NET_ADMIN
11
+
ports:
12
+
- "80:80"
13
+
- "443:443"
14
+
- "443:443/udp"
15
+
volumes:
16
+
- ./Caddyfile:/etc/caddy/Caddyfile
17
+
- ./cert/localtangled:/data/pki/authorities/localtangled
18
+
- caddy_data:/data
19
+
- caddy_config:/config
20
+
21
+
plc:
22
+
image: ghcr.io/bluesky-social/did-method-plc:plc-f2ab7516bac5bc0f3f86842fa94e996bd1b3815b
23
+
# did-method-plc only provides linux/amd64
24
+
platform: linux/amd64
25
+
container_name: plc
26
+
restart: unless-stopped
27
+
depends_on:
28
+
- plc_db
29
+
environment:
30
+
DEBUG_MODE: 1
31
+
LOG_ENABLED: "true"
32
+
LOG_LEVEL: "debug"
33
+
LOG_DESTINATION: 1
34
+
DB_CREDS_JSON: &DB_CREDS_JSON '{"username":"pg","password":"password","host":"plc_db","port":5432}'
35
+
DB_MIGRATE_CREDS_JSON: *DB_CREDS_JSON
36
+
PLC_VERSION: 0.0.1
37
+
PORT: 8080
38
+
39
+
plc_db:
40
+
image: postgres:14.4-alpine
41
+
container_name: plc_db
42
+
environment:
43
+
- POSTGRES_USER=pg
44
+
- POSTGRES_PASSWORD=password
45
+
- PGPORT=5432
46
+
volumes:
47
+
- plc:/var/lib/postgresql/data
48
+
49
+
pds:
50
+
container_name: pds
51
+
image: ghcr.io/bluesky-social/pds:0.4
52
+
restart: unless-stopped
53
+
volumes:
54
+
- pds:/pds
55
+
env_file:
56
+
- ./pds.env
57
+
58
+
# I can change the knot-docker and spindle-docker images,
59
+
# which means I can inject the cert to those containers
60
+
#
61
+
# so define *.tngl.boltless.dev as extra_hosts & inject certs to those two containers
62
+
# extra_hosts:
63
+
# plc.tngl.boltless.dev:host.docker.internal
64
+
65
+
jetstream:
66
+
container_name: jetstream
67
+
image: ghcr.io/bluesky-social/jetstream:sha-0ab10bd
68
+
restart: unless-stopped
69
+
volumes:
70
+
- jetstream:/data
71
+
environment:
72
+
- JETSTREAM_DATA_DIR=/data
73
+
# livness check interval to restart when no events are received (default: 15sec)
74
+
- JETSTREAM_LIVENESS_TTL=300s
75
+
- JETSTREAM_WS_URL=ws://pds:3000/xrpc/com.atproto.sync.subscribeRepos
76
+
77
+
volumes:
78
+
caddy_config:
79
+
caddy_data:
80
+
plc:
81
+
pds:
82
+
jetstream:
+17
local-infra/pds.env
+17
local-infra/pds.env
···
1
+
PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a
2
+
PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3
3
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7
4
+
5
+
LOG_ENABLED=true
6
+
7
+
# PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
8
+
# PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
9
+
10
+
PDS_DATA_DIRECTORY=/pds
11
+
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
12
+
13
+
PDS_DID_PLC_URL=http://plc:8080
14
+
PDS_HOSTNAME=pds.tngl.boltless.dev
15
+
16
+
# PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
17
+
# PDS_REPORT_SERVICE_URL=https://mod.bsky.app
+9
local-infra/readme.md
+9
local-infra/readme.md
+63
local-infra/scripts/create-test-account.sh
+63
local-infra/scripts/create-test-account.sh
···
1
+
#!/bin/bash
2
+
set -o errexit
3
+
set -o nounset
4
+
set -o pipefail
5
+
6
+
source "$(dirname "$0")/../pds.env"
7
+
8
+
# curl a URL and fail if the request fails.
9
+
function curl_cmd_get {
10
+
curl --fail --silent --show-error "$@"
11
+
}
12
+
13
+
# curl a URL and fail if the request fails.
14
+
function curl_cmd_post {
15
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
16
+
}
17
+
18
+
# curl a URL but do not fail if the request fails.
19
+
function curl_cmd_post_nofail {
20
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
21
+
}
22
+
23
+
USERNAME="${1:-}"
24
+
25
+
if [[ "${USERNAME}" == "" ]]; then
26
+
read -p "Enter a username: " USERNAME
27
+
fi
28
+
29
+
if [[ "${USERNAME}" == "" ]]; then
30
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
31
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
32
+
exit 1
33
+
fi
34
+
35
+
PASSWORD="password"
36
+
INVITE_CODE="$(curl_cmd_post \
37
+
--user "admin:${PDS_ADMIN_PASSWORD}" \
38
+
--data '{"useCount": 1}' \
39
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code'
40
+
)"
41
+
RESULT="$(curl_cmd_post_nofail \
42
+
--data "{\"email\":\"${USERNAME}@${PDS_HOSTNAME}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \
43
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount"
44
+
)"
45
+
46
+
DID="$(echo $RESULT | jq --raw-output '.did')"
47
+
if [[ "${DID}" != did:* ]]; then
48
+
ERR="$(echo ${RESULT} | jq --raw-output '.message')"
49
+
echo "ERROR: ${ERR}" >/dev/stderr
50
+
echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr
51
+
exit 1
52
+
fi
53
+
54
+
echo
55
+
echo "Account created successfully!"
56
+
echo "-----------------------------"
57
+
echo "Handle : ${USERNAME}.${PDS_HOSTNAME}"
58
+
echo "DID : ${DID}"
59
+
echo "Password : ${PASSWORD}"
60
+
echo "-----------------------------"
61
+
echo "This is a test account with an insecure password."
62
+
echo "Make sure it's only used for development."
63
+
echo
+27
nix/modules/knot.nix
+27
nix/modules/knot.nix
···
111
111
description = "Hostname for the server (required)";
112
112
};
113
113
114
+
plcUrl = mkOption {
115
+
type = types.str;
116
+
default = "https://plc.directory";
117
+
description = "atproto PLC directory";
118
+
};
119
+
120
+
jetstreamEndpoint = mkOption {
121
+
type = types.str;
122
+
default = "wss://jetstream1.us-west.bsky.network/subscribe";
123
+
description = "Jetstream endpoint to subscribe to";
124
+
};
125
+
114
126
dev = mkOption {
115
127
type = types.bool;
116
128
default = false;
···
145
157
'';
146
158
};
147
159
160
+
# TODO: abstract this to share same env table with systemd.services.knot
161
+
environment.variables = {
162
+
"KNOT_REPO_SCAN_PATH" = cfg.repo.scanPath;
163
+
"KNOT_REPO_MAIN_BRANCH" = cfg.repo.mainBranch;
164
+
"APPVIEW_ENDPOINT" = cfg.appviewEndpoint;
165
+
"KNOT_SERVER_INTERNAL_LISTEN_ADDR" = cfg.server.internalListenAddr;
166
+
"KNOT_SERVER_LISTEN_ADDR" = cfg.server.listenAddr;
167
+
"KNOT_SERVER_DB_PATH" = cfg.server.dbPath;
168
+
"KNOT_SERVER_HOSTNAME" = cfg.server.hostname;
169
+
"KNOT_SERVER_PLC_URL" = cfg.server.plcUrl;
170
+
"KNOT_SERVER_JETSTREAM_ENDPOINT" = cfg.server.jetstreamEndpoint;
171
+
"KNOT_SERVER_OWNER" = cfg.server.owner;
172
+
};
148
173
environment.etc."ssh/keyfetch_wrapper" = {
149
174
mode = "0555";
150
175
text = ''
···
199
224
"KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}"
200
225
"KNOT_SERVER_DB_PATH=${cfg.server.dbPath}"
201
226
"KNOT_SERVER_HOSTNAME=${cfg.server.hostname}"
227
+
"KNOT_SERVER_PLC_URL=${cfg.server.plcUrl}"
228
+
"KNOT_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}"
202
229
"KNOT_SERVER_OWNER=${cfg.server.owner}"
203
230
];
204
231
ExecStart = "${cfg.package}/bin/knot server";
+8
-1
nix/modules/spindle.nix
+8
-1
nix/modules/spindle.nix
···
37
37
description = "Hostname for the server (required)";
38
38
};
39
39
40
+
plcUrl = mkOption {
41
+
type = types.str;
42
+
default = "https://plc.directory";
43
+
description = "atproto PLC directory";
44
+
};
45
+
40
46
jetstreamEndpoint = mkOption {
41
47
type = types.str;
42
48
default = "wss://jetstream1.us-west.bsky.network/subscribe";
···
119
125
"SPINDLE_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}"
120
126
"SPINDLE_SERVER_DB_PATH=${cfg.server.dbPath}"
121
127
"SPINDLE_SERVER_HOSTNAME=${cfg.server.hostname}"
122
-
"SPINDLE_SERVER_JETSTREAM=${cfg.server.jetstreamEndpoint}"
128
+
"SPINDLE_SERVER_PLC_URL=${cfg.server.plcUrl}"
129
+
"SPINDLE_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}"
123
130
"SPINDLE_SERVER_DEV=${lib.boolToString cfg.server.dev}"
124
131
"SPINDLE_SERVER_OWNER=${cfg.server.owner}"
125
132
"SPINDLE_SERVER_MAX_JOB_COUNT=${toString cfg.server.maxJobCount}"
+18
-2
nix/vm.nix
+18
-2
nix/vm.nix
···
10
10
if var == ""
11
11
then throw "\$${name} must be defined, see docs/hacking.md for more details"
12
12
else var;
13
+
envVarOr = name: default: let
14
+
var = builtins.getEnv name;
15
+
in
16
+
if var != "" then var else default;
17
+
18
+
plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory";
19
+
jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe";
13
20
in
14
21
nixpkgs.lib.nixosSystem {
15
22
inherit system;
···
70
77
};
71
78
# This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall
72
79
networking.firewall.enable = false;
80
+
services.dnsmasq.enable = true;
81
+
services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2";
82
+
security.pki.certificates = [
83
+
(builtins.readFile ../local-infra/cert/localtangled/root.crt)
84
+
];
73
85
time.timeZone = "Europe/London";
74
86
services.getty.autologinUser = "root";
75
87
environment.systemPackages = with pkgs; [curl vim git sqlite litecli];
···
78
90
motd = "Welcome to the development knot!\n";
79
91
server = {
80
92
owner = envVar "TANGLED_VM_KNOT_OWNER";
81
-
hostname = "localhost:6000";
93
+
hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6000";
94
+
plcUrl = plcUrl;
95
+
jetstreamEndpoint = jetstream;
82
96
listenAddr = "0.0.0.0:6000";
83
97
};
84
98
};
···
86
100
enable = true;
87
101
server = {
88
102
owner = envVar "TANGLED_VM_SPINDLE_OWNER";
89
-
hostname = "localhost:6555";
103
+
hostname = envVarOr "TANGLED_VM_SPINDLE_OWNER" "localhost:6555";
104
+
plcUrl = plcUrl;
105
+
jetstreamEndpoint = jetstream;
90
106
listenAddr = "0.0.0.0:6555";
91
107
dev = true;
92
108
queueSize = 100;
+1
spindle/config/config.go
+1
spindle/config/config.go
···
13
13
DBPath string `env:"DB_PATH, default=spindle.db"`
14
14
Hostname string `env:"HOSTNAME, required"`
15
15
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
+
PlcUrl string `env:"PLC_URL, default=plc.directory"`
16
17
Dev bool `env:"DEV, default=false"`
17
18
Owner string `env:"OWNER, required"`
18
19
Secrets Secrets `env:",prefix=SECRETS_"`
+3
-7
spindle/ingester.go
+3
-7
spindle/ingester.go
···
9
9
10
10
"tangled.org/core/api/tangled"
11
11
"tangled.org/core/eventconsumer"
12
-
"tangled.org/core/idresolver"
13
12
"tangled.org/core/rbac"
14
13
"tangled.org/core/spindle/db"
15
14
···
142
141
func (s *Spindle) ingestRepo(ctx context.Context, e *models.Event) error {
143
142
var err error
144
143
did := e.Did
145
-
resolver := idresolver.DefaultResolver()
146
144
147
145
l := s.l.With("component", "ingester", "record", tangled.RepoNSID)
148
146
···
190
188
}
191
189
192
190
// add collaborators to rbac
193
-
owner, err := resolver.ResolveIdent(ctx, did)
191
+
owner, err := s.res.ResolveIdent(ctx, did)
194
192
if err != nil || owner.Handle.IsInvalidHandle() {
195
193
return err
196
194
}
···
225
223
return err
226
224
}
227
225
228
-
resolver := idresolver.DefaultResolver()
229
-
230
-
subjectId, err := resolver.ResolveIdent(ctx, record.Subject)
226
+
subjectId, err := s.res.ResolveIdent(ctx, record.Subject)
231
227
if err != nil || subjectId.Handle.IsInvalidHandle() {
232
228
return err
233
229
}
···
240
236
241
237
// TODO: get rid of this entirely
242
238
// resolve this aturi to extract the repo record
243
-
owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
239
+
owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String())
244
240
if err != nil || owner.Handle.IsInvalidHandle() {
245
241
return fmt.Errorf("failed to resolve handle: %w", err)
246
242
}