···44 "context"
55 "fmt"
6677- "github.com/bluesky-social/indigo/atproto/syntax"
87 blocks "github.com/ipfs/go-block-format"
98 "github.com/ipfs/go-cid"
109 "gorm.io/gorm/clause"
···1716type SqliteBlockstore struct {
1817 db *db.DB
1918 did string
1919+ rev string
2020 readonly bool
2121 inserts map[cid.Cid]blocks.Block
2222}
···3939 }
4040}
41414242+// SetRev sets the revision that will be stamped on every block written to the
4343+// store. It should be called with the new repo revision before any Put/PutMany
4444+// calls for a given commit.
4545+func (bs *SqliteBlockstore) SetRev(rev string) {
4646+ bs.rev = rev
4747+}
4848+4249func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) {
4350 var block models.Block
4451···6976 b := models.Block{
7077 Did: bs.did,
7178 Cid: block.Cid().Bytes(),
7272- Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
7979+ Rev: bs.rev,
7380 Value: block.RawData(),
7481 }
7582···108115 b := models.Block{
109116 Did: bs.did,
110117 Cid: block.Cid().Bytes(),
111111- Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
118118+ Rev: bs.rev,
112119 Value: block.RawData(),
113120 }
114121
+63-21
oauth/client/manager.go
···1818 "pkg.rbrt.fr/vow/internal/helpers"
1919)
20202121+// supportedScopes lists the OAuth scopes this server accepts.
2222+var supportedScopes = []string{"atproto", "transition:generic", "transition:chat.bsky"}
2323+2124type Manager struct {
2225 cli *http.Client
2326 logger *slog.Logger
···5962 var jwks jwk.Key
6063 if metadata.TokenEndpointAuthMethod == "private_key_jwt" {
6164 if metadata.JWKS != nil && len(metadata.JWKS.Keys) > 0 {
6262- // TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
6363- // make sure we use the right one
6464- b, err := json.Marshal(metadata.JWKS.Keys[0])
6565- if err != nil {
6666- return nil, err
6767- }
6868-6969- k, err := helpers.ParseJWKFromBytes(b)
6565+ k, err := selectKey(metadata.JWKS.Keys, metadata.TokenEndpointAuthSigningAlg)
7066 if err != nil {
7167 return nil, err
7268 }
7373-7469 jwks = k
7570 } else if metadata.JWKSURI != nil {
7671 maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
···147142 }
148143149144 type Keys struct {
150150- Keys []map[string]any `json:"keys"`
145145+ Keys []any `json:"keys"`
151146 }
152147153148 var keys Keys
···159154 return nil, errors.New("no keys in jwks response")
160155 }
161156162162- // TODO: this is again bad, we should be figuring out which one we need to use...
163163- b, err := json.Marshal(keys.Keys[0])
164164- if err != nil {
165165- return nil, fmt.Errorf("could not marshal key: %w", err)
166166- }
167167-168168- k, err := helpers.ParseJWKFromBytes(b)
157157+ k, err := selectKey(keys.Keys, "")
169158 if err != nil {
170159 return nil, err
171160 }
···176165 return jwks, nil
177166}
178167168168+// selectKey picks the best signing key from a raw JWKS key list.
169169+// It prefers a key whose "kid" matches the hint (if non-empty), then any key
170170+// with "use"="sig", and finally falls back to the first key in the set.
171171+func selectKey(keys []any, kidHint string) (jwk.Key, error) {
172172+ if len(keys) == 0 {
173173+ return nil, errors.New("empty jwks")
174174+ }
175175+176176+ asMap := func(v any) map[string]any {
177177+ m, _ := v.(map[string]any)
178178+ return m
179179+ }
180180+181181+ var chosen map[string]any
182182+183183+ if kidHint != "" {
184184+ for _, k := range keys {
185185+ m := asMap(k)
186186+ if m["kid"] == kidHint {
187187+ chosen = m
188188+ break
189189+ }
190190+ }
191191+ }
192192+193193+ if chosen == nil {
194194+ for _, k := range keys {
195195+ m := asMap(k)
196196+ if use, _ := m["use"].(string); use == "sig" {
197197+ chosen = m
198198+ break
199199+ }
200200+ }
201201+ }
202202+203203+ if chosen == nil {
204204+ chosen = asMap(keys[0])
205205+ }
206206+207207+ if chosen == nil {
208208+ return nil, errors.New("jwks contains no usable keys")
209209+ }
210210+211211+ b, err := json.Marshal(chosen)
212212+ if err != nil {
213213+ return nil, fmt.Errorf("could not marshal key: %w", err)
214214+ }
215215+216216+ return helpers.ParseJWKFromBytes(b)
217217+}
218218+179219func validateAndParseMetadata(clientId string, b []byte) (*Metadata, error) {
180220 var metadataMap map[string]any
181221 if err := json.Unmarshal(b, &metadataMap); err != nil {
···246286 return nil, fmt.Errorf("duplicate scope `%s`", scope)
247287 }
248288249249- // TODO: check for unsupported scopes
289289+ if !slices.Contains(supportedScopes, scope) {
290290+ return nil, fmt.Errorf("unsupported scope %q", scope)
291291+ }
250292251293 scopesMap[scope] = true
252294 }
···259301260302 switch gt {
261303 case "implicit":
262262- return nil, errors.New("grantg type `implicit` is not allowed")
304304+ return nil, errors.New("grant type `implicit` is not allowed")
263305 case "authorization_code", "refresh_token":
264264- // TODO check if this grant type is supported
306306+ // supported
265307 default:
266266- return nil, fmt.Errorf("grant tyhpe `%s` is not supported", gt)
308308+ return nil, fmt.Errorf("grant type `%s` is not supported", gt)
267309 }
268310269311 grantTypesMap[gt] = true
+6-6
readme.md
···53534. **Start the services**
54545555 ```bash
5656- docker-compose pull
5757- docker-compose up -d
5656+ docker compose pull
5757+ docker compose up -d
5858 ```
595960605. **Get your invite code**
···6262 On first run, an invite code is automatically created. View it with:
63636464 ```bash
6565- docker-compose logs create-invite
6565+ docker compose logs create-invite
6666 ```
67676868 Or check the saved file:
···737374746. **Monitor the services**
7575 ```bash
7676- docker-compose logs -f
7676+ docker compose logs -f
7777 ```
78787979### What Gets Set Up
···146146## Updating
147147148148```bash
149149-docker-compose pull
150150-docker-compose up -d
149149+docker compose build
150150+docker compose up -d
151151```
152152153153## Implemented Endpoints
···6262 return
6363 }
64646565- // TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
6666- dpopProof, err := s.oauthProvider.DpopManager.CheckProof(r.Method, "https://"+s.config.Hostname+r.URL.String(), r.Header, nil)
6565+ scheme := "https"
6666+ if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") == "" {
6767+ scheme = "http"
6868+ } else if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
6969+ scheme = proto
7070+ }
7171+ dpopProof, err := s.oauthProvider.DpopManager.CheckProof(r.Method, scheme+"://"+r.Host+r.URL.String(), r.Header, nil)
6772 if err != nil {
6873 if errors.Is(err, dpop.ErrUseDpopNonce) {
6974 nonce := s.oauthProvider.NextNonce()
-6
server/handle_oauth_token.go
···100100 return
101101 }
102102103103- // TODO: this should come from an oauth provider config
104104- if !slices.Contains([]string{"authorization_code", "refresh_token"}, req.GrantType) {
105105- helpers.InputError(w, new(fmt.Sprintf(`"%s" grant type is not supported by the server`, req.GrantType)))
106106- return
107107- }
108108-109103 if !slices.Contains(client.Metadata.GrantTypes, req.GrantType) {
110104 helpers.InputError(w, new(fmt.Sprintf(`"%s" grant type is not supported by the client`, req.GrantType)))
111105 return
+30-6
server/handle_repo_list_repos.go
···2233import (
44 "net/http"
55+ "strconv"
5666- "pkg.rbrt.fr/vow/models"
77 "github.com/ipfs/go-cid"
88+ "pkg.rbrt.fr/vow/internal/helpers"
99+ "pkg.rbrt.fr/vow/models"
810)
9111012type ComAtprotoSyncListReposResponse struct {
···2022 Status *string `json:"status,omitempty"`
2123}
22242323-// TODO: paginate this bitch
2425func (s *Server) handleListRepos(w http.ResponseWriter, r *http.Request) {
2526 ctx := r.Context()
26272828+ limit := 500
2929+ if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
3030+ if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 {
3131+ limit = l
3232+ }
3333+ }
3434+3535+ cursor := r.URL.Query().Get("cursor")
3636+3737+ params := []any{}
3838+ cursorClause := ""
3939+ if cursor != "" {
4040+ cursorClause = "WHERE did > ?"
4141+ params = append(params, cursor)
4242+ }
4343+ params = append(params, limit+1)
4444+2745 var repos []models.Repo
2828- if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
2929- http.Error(w, err.Error(), http.StatusInternalServerError)
4646+ if err := s.db.Raw(ctx, "SELECT * FROM repos "+cursorClause+" ORDER BY did ASC LIMIT ?", nil, params...).Scan(&repos).Error; err != nil {
4747+ helpers.ServerError(w, nil)
3048 return
3149 }
32505151+ var nextCursor *string
5252+ if len(repos) > limit {
5353+ repos = repos[:limit]
5454+ nextCursor = &repos[len(repos)-1].Did
5555+ }
5656+3357 items := make([]ComAtprotoSyncListReposRepoItem, 0, len(repos))
3458 for _, repo := range repos {
3559 c, err := cid.Cast(repo.Root)
3660 if err != nil {
3737- http.Error(w, err.Error(), http.StatusInternalServerError)
6161+ helpers.ServerError(w, nil)
3862 return
3963 }
4064···4872 }
49735074 s.writeJSON(w, 200, ComAtprotoSyncListReposResponse{
5151- Cursor: nil,
7575+ Cursor: nextCursor,
5276 Repos: items,
5377 })
5478}
···2424 return
2525 }
26262727- // TODO: add tid param
2827 cursor := r.URL.Query().Get("cursor")
29283029 limit := 50
···38373938 params := []any{did}
4039 if cursor != "" {
4141- params = append(params, cursor)
4242- cursorquery = "AND created_at < ?"
4040+ // cursor is the CID string of the last blob on the previous page;
4141+ // convert to bytes for the comparison against the binary cid column.
4242+ cursorCid, err := cid.Decode(cursor)
4343+ if err != nil {
4444+ helpers.InputError(w, new("invalid cursor"))
4545+ return
4646+ }
4747+ params = append(params, cursorCid.Bytes())
4848+ cursorquery = "AND cid > ?"
4349 }
4444- params = append(params, limit)
5050+ params = append(params, limit+1)
45514652 urepo, err := s.getRepoActorByDid(ctx, did)
4753 if err != nil {
···5965 }
60666167 var blobs []models.Blob
6262- if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
6868+ if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY cid ASC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
6369 logger.Error("error getting records", "error", err)
6470 helpers.ServerError(w, nil)
6571 return
···8086 }
81878288 var newcursor *string
8383- if len(blobs) == limit {
8484- newcursor = &blobs[len(blobs)-1].CreatedAt
8989+ if len(blobs) > limit {
9090+ blobs = blobs[:limit]
9191+ lastCid, err := cid.Cast(blobs[len(blobs)-1].Cid)
9292+ if err == nil {
9393+ s := lastCid.String()
9494+ newcursor = &s
9595+ }
8596 }
86978798 s.writeJSON(w, http.StatusOK, ComAtprotoSyncListBlobsResponse{
+18-8
server/repo.go
···169169 }, nil
170170}
171171172172-func commitRepo(ctx context.Context, bs blockstore.Blockstore, r *atp.Repo, signingKey []byte) (cid.Cid, string, error) {
173173- if _, err := r.MST.WriteDiffBlocks(ctx, bs.(legacyblockstore.Blockstore)); err != nil { //nolint:staticcheck
174174- return cid.Undef, "", fmt.Errorf("writing MST blocks: %w", err)
175175- }
172172+// revSetter is implemented by blockstores that can be told the current repo
173173+// revision before blocks are written (so the Rev column is stamped correctly).
174174+type revSetter interface {
175175+ SetRev(rev string)
176176+}
176177178178+func commitRepo(ctx context.Context, bs blockstore.Blockstore, r *atp.Repo, signingKey []byte) (cid.Cid, string, error) {
177179 commit, err := r.Commit()
178180 if err != nil {
179181 return cid.Undef, "", fmt.Errorf("creating commit: %w", err)
···187189 return cid.Undef, "", fmt.Errorf("signing commit: %w", err)
188190 }
189191192192+ // Stamp the revision on the blockstore before writing any blocks so that
193193+ // every block persisted for this commit carries the correct Rev value.
194194+ if rs, ok := bs.(revSetter); ok {
195195+ rs.SetRev(commit.Rev)
196196+ }
197197+198198+ if _, err := r.MST.WriteDiffBlocks(ctx, bs.(legacyblockstore.Blockstore)); err != nil { //nolint:staticcheck
199199+ return cid.Undef, "", fmt.Errorf("writing MST blocks: %w", err)
200200+ }
201201+190202 buf := new(bytes.Buffer)
191203 if err := commit.MarshalCBOR(buf); err != nil {
192204 return cid.Undef, "", fmt.Errorf("marshaling commit: %w", err)
···331343 return cid.Undef, err
332344 }
333345334334- // TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we
335335- // check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical
336336- // when reading this code. i dont feel like fixing right now though so
346346+ // A nil Cid on the entry is the sentinel used later in the
347347+ // batch-upsert loop to distinguish deletes from creates/updates.
337348 entries = append(entries, models.Record{
338349 Did: urepo.Did,
339350 Nsid: op.Collection,
···504515 return nil, err
505516 }
506517507507- // TODO:
508518 cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value)
509519 if err != nil {
510520 return nil, err