This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

docs

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jul 29, 2026, 5:38 PM +0900) commit d724c161 parent cf7b0726 change-id qlqqprmr
+109 -1
+1
docker-compose.yml
··· 169 169 restart: unless-stopped 170 170 environment: 171 171 SPINDLE_SERVER_HOSTNAME: spindle.tngl.boltless.dev 172 + SPINDLE_SERVER_ADMIN_PASSWORD: spindle-admin 172 173 SPINDLE_SERVER_LISTEN_ADDR: 0.0.0.0:6555 173 174 SPINDLE_SERVER_DB_PATH: /var/lib/spindle/spindle.db 174 175 SPINDLE_SERVER_PLC_URL: https://plc.tngl.boltless.dev
+20
docs/DOCS.md
··· 1451 1451 - `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`). 1452 1452 - `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`). 1453 1453 - `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required). 1454 + - `SPINDLE_SERVER_ADMIN_PASSWORD`: The password for the `/admin` endpoints used by `spindle admin allow|block` to manage members. When unset, the `/admin` endpoints are not mounted at all (default: `""`). 1454 1455 - `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`). 1455 1456 - `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`). 1456 1457 - `SPINDLE_SERVER_INVITE_ONLY`: A boolean indicating whether only members of this spindle may register repos. When `false`, the spindle accepts repos from anyone on the network (default: `true`). ··· 1540 1541 ``` 1541 1542 1542 1543 Spindle will now start, connect to the Jetstream server, and begin processing pipelines. 1544 + 1545 + ### Managing members 1546 + 1547 + An invite-only spindle (the default, see `SPINDLE_SERVER_INVITE_ONLY`) only 1548 + accepts repos whose owner is a member. Members are managed over the `/admin` 1549 + endpoints, which authenticate with `SPINDLE_SERVER_ADMIN_PASSWORD`: 1550 + 1551 + ```shell 1552 + export SPINDLE_SERVER_ADMIN_PASSWORD="your-admin-password" 1553 + spindle admin --url http://localhost:6555 allow did:plc:examplemember 1554 + spindle admin --url http://localhost:6555 block did:plc:examplemember 1555 + ``` 1556 + 1557 + Blocking keeps the DID on record and denies it, so a blocked DID stays blocked 1558 + until you allow it again. 1559 + 1560 + If `SPINDLE_SERVER_ADMIN_PASSWORD` is unset the spindle still starts, but the 1561 + `/admin` endpoints are not mounted (they return 404) and `spindle admin` cannot 1562 + be used, so set it if you run an invite-only spindle. 1543 1563 1544 1564 ### Running microVM workflows 1545 1565
+3 -1
nix/modules/spindle.nix
··· 299 299 description = '' 300 300 Additional environment file as defined in {manpage}`systemd.exec(5)`. 301 301 302 - Sensitive secrets such as {env}`AWS_SECRET_ACCESS_KEY`, 302 + Sensitive secrets such as {env}`SPINDLE_SERVER_ADMIN_PASSWORD` 303 + (without it the `/admin` member management endpoints are not mounted), 304 + {env}`AWS_SECRET_ACCESS_KEY`, 303 305 {env}`AWS_ACCESS_KEY_ID`, {env}`AWS_REGION` 304 306 may be passed to the service 305 307 without making them world readable in the nix store.
+85
spindle/admin_test.go
··· 1 + package spindle 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "net/http/httptest" 7 + "testing" 8 + 9 + "tangled.org/core/idresolver" 10 + "tangled.org/core/spindle/config" 11 + ) 12 + 13 + func TestAdminRouterMountedOnlyWhenConfigured(t *testing.T) { 14 + tests := []struct { 15 + name string 16 + password string 17 + want int 18 + }{ 19 + // mounted: rejects the unauthenticated request instead of 404ing 20 + {"configured", "s3cret", http.StatusUnauthorized}, 21 + {"unset password", "", http.StatusNotFound}, 22 + } 23 + 24 + for _, tt := range tests { 25 + t.Run(tt.name, func(t *testing.T) { 26 + s := &Spindle{ 27 + l: slog.Default(), 28 + res: idresolver.DefaultResolver("https://plc.test"), 29 + cfg: &config.Config{Server: config.Server{ 30 + AdminPassword: tt.password, 31 + Hostname: "spindle.test", 32 + }}, 33 + } 34 + 35 + req := httptest.NewRequest(http.MethodPost, "/admin/member/allow", nil) 36 + w := httptest.NewRecorder() 37 + s.Router().ServeHTTP(w, req) 38 + 39 + if w.Code != tt.want { 40 + t.Errorf("status = %d, want %d", w.Code, tt.want) 41 + } 42 + }) 43 + } 44 + } 45 + 46 + func TestAdminMiddleware(t *testing.T) { 47 + tests := []struct { 48 + name string 49 + configured string 50 + user, pass string 51 + basicAuth bool 52 + want int 53 + }{ 54 + {"correct password", "s3cret", "admin", "s3cret", true, http.StatusOK}, 55 + {"wrong password", "s3cret", "admin", "nope", true, http.StatusUnauthorized}, 56 + {"wrong user", "s3cret", "root", "s3cret", true, http.StatusUnauthorized}, 57 + {"no credentials", "s3cret", "", "", false, http.StatusUnauthorized}, 58 + // Router() skips the mount when the password is unset, but keep the middleware 59 + // fail-closed too - an empty password must not authenticate anyone. 60 + {"unset password", "", "admin", "", true, http.StatusUnauthorized}, 61 + {"unset password, no credentials", "", "", "", false, http.StatusUnauthorized}, 62 + } 63 + 64 + for _, tt := range tests { 65 + t.Run(tt.name, func(t *testing.T) { 66 + s := &Spindle{cfg: &config.Config{ 67 + Server: config.Server{AdminPassword: tt.configured}, 68 + }} 69 + 70 + req := httptest.NewRequest(http.MethodPost, "/admin/member/allow", nil) 71 + if tt.basicAuth { 72 + req.SetBasicAuth(tt.user, tt.pass) 73 + } 74 + w := httptest.NewRecorder() 75 + 76 + s.adminMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 + w.WriteHeader(http.StatusOK) 78 + })).ServeHTTP(w, req) 79 + 80 + if w.Code != tt.want { 81 + t.Errorf("status = %d, want %d", w.Code, tt.want) 82 + } 83 + }) 84 + } 85 + }