forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

Compare changes

Choose any two refs to compare.

+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
··· 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
··· 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
··· 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
··· 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
··· 26 26 subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject) 27 27 if err != nil { 28 28 log.Println("failed to follow, invalid did") 29 - return 30 29 } 31 30 32 31 if currentUser.Did == subjectIdent.DID.String() {
+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
··· 44 44 45 45 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 46 46 if err != nil { 47 + l.Error("failed to start auth", "err", err) 47 48 http.Error(w, err.Error(), http.StatusInternalServerError) 48 49 return 49 50 }
+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
··· 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 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
··· 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
··· 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
··· 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
··· 36 36 l: log.FromContext(ctx), 37 37 jc: jc, 38 38 n: n, 39 - resolver: idresolver.DefaultResolver(), 39 + resolver: idresolver.DefaultResolver(c.Server.PlcUrl), 40 40 } 41 41 42 42 err := e.AddKnot(rbac.ThisServer)
+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
··· 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
··· 1 + -----BEGIN EC PRIVATE KEY----- 2 + MHcCAQEEIB1EH4KZGLcfO0neWDuV3oWMXPEze8JTsyKFoQuYApFSoAoGCCqGSM49 3 + AwEHoUQDQgAEbX+zyr9rLxF3E8oCZwJluCKX/xmU4waabkjaTGbI5K0cemiAAmZR 4 + J2lVhgh+KfXDPpTmt+YE6FUF4xAWADOUuQ== 5 + -----END EC PRIVATE KEY-----
+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
··· 1 + -----BEGIN EC PRIVATE KEY----- 2 + MHcCAQEEIBqEj1iG3q+OLBgHjWQ3UkvKjq4sy5ej47syIYWn/Ql/oAoGCCqGSM49 3 + AwEHoUQDQgAE7rFM4oNfT0UMqMuc3L60TCLeTd58WFSUYnKl7R1HOHDWeWZhhoNd 4 + WguXJSHhFPiWmQ5E+fiI7KvDAVQGHzfUAg== 5 + -----END EC PRIVATE KEY-----
+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
··· 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
··· 1 + run compose 2 + ``` 3 + docker compose up -d 4 + ``` 5 + 6 + trust the cert (macOS) 7 + ``` 8 + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./local-infra/cert/localtangled/root.crt 9 + ```
+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
··· 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
··· 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
··· 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
··· 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
··· 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 }
+1 -1
spindle/server.go
··· 123 123 jc.AddDid(d) 124 124 } 125 125 126 - resolver := idresolver.DefaultResolver() 126 + resolver := idresolver.DefaultResolver(cfg.Server.PlcUrl) 127 127 128 128 spindle := Spindle{ 129 129 jc: jc,