Monorepo for Tangled
0
fork

Configure Feed

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

contrib,nix: local, sandboxed atmosphere infra

Add sandboxed atmosphere environment for local testing. This new vm
contains everything required to run local test appview including PLC,
PDS, Jetstream (listening to single PDS), knot and spindle.

I'm using my custom `tngl.boltless.dev` domain which resolves to
`127.0.0.1` without any proxy.

PLC: plc.tngl.boltless.dev
PDS: pds.tngl.boltless.dev
Relay: relay.tngl.boltless.dev
Jetstream: jetstream.tngl.boltless.dev
Knot: knot.tngl.boltless.dev
Spindle: spindle.tngl.boltless.dev

TLS is supported with caddy service running inside the vm.

note: `pds.env` file here is hard copy to be used for contrib/scripts.
note: upgraded pds package in order to set email settings

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

+389 -2
+11
contrib/certs/root.crt
··· 1 + -----BEGIN CERTIFICATE----- 2 + MIIBpDCCAUqgAwIBAgIRAMDJmvzd+r8Ksn8NANvz0scwCgYIKoZIzj0EAwIwMDEu 3 + MCwGA1UEAxMlQ2FkZHkgTG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDAe 4 + Fw0yNjA0MTQxMTE1MTNaFw0zNjAyMjExMTE1MTNaMDAxLjAsBgNVBAMTJUNhZGR5 5 + IExvY2FsIEF1dGhvcml0eSAtIDIwMjYgRUNDIFJvb3QwWTATBgcqhkjOPQIBBggq 6 + hkjOPQMBBwNCAARuhaTbFX02t5dxCroU2SfdX/7YBE04C5iZPx4+j80nyFqceoyX 7 + nPKIYOwJ/g/RjOrIcZTsgbKOPF9CU4QAPDijo0UwQzAOBgNVHQ8BAf8EBAMCAQYw 8 + EgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU/RepkPZnmDAXtnSmK9Bpq+ki 9 + 0i0wCgYIKoZIzj0EAwIDSAAwRQIgMqkop//6OowjqFpJL6z/fJ/TAh2Kj8yRKgU9 10 + /ZA5sSUCIQCHXKeNS+NuAE5brWmQcgNI+Gn0/43TtjQM5F/uiJ1f7A== 11 + -----END CERTIFICATE-----
+33
contrib/example.env
··· 1 + # NOTE: put actual DIDs here 2 + alice_did=did:plc:alice-did 3 + tangled_did=did:plc:tangled-did 4 + 5 + #core 6 + export TANGLED_DEV=true 7 + export TANGLED_APPVIEW_HOST=127.0.0.1:3000 8 + # plc 9 + export TANGLED_PLC_URL=https://plc.tngl.boltless.dev 10 + # jetstream 11 + export TANGLED_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe 12 + # label 13 + export TANGLED_LABEL_GFI=at://${tangled_did}/sh.tangled.label.definition/good-first-issue 14 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_GFI 15 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/assignee 16 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/documentation 17 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/duplicate 18 + export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/wontfix 19 + 20 + # vm settings 21 + export TANGLED_VM_PLC_URL=https://plc.tngl.boltless.dev 22 + export TANGLED_VM_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe 23 + export TANGLED_VM_KNOT_HOST=knot.tngl.boltless.dev 24 + export TANGLED_VM_KNOT_OWNER=$alice_did 25 + export TANGLED_VM_SPINDLE_HOST=spindle.tngl.boltless.dev 26 + export TANGLED_VM_SPINDLE_OWNER=$alice_did 27 + 28 + if [ -n "${TANGLED_RESEND_API_KEY:-}" ] && [ -n "${TANGLED_RESEND_SENT_FROM:-}" ]; then 29 + export TANGLED_VM_PDS_EMAIL_SMTP_URL=smtps://resend:$TANGLED_RESEND_API_KEY@smtp.resend.com:465/ 30 + export TANGLED_VM_PDS_EMAIL_FROM_ADDRESS=$TANGLED_RESEND_SENT_FROM 31 + fi 32 + 33 + export TANGLED_KNOTMIRROR_URL=https://mirror.tngl.boltless.dev
+12
contrib/pds.env
··· 1 + LOG_ENABLED=true 2 + 3 + PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a 4 + PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3 5 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7 6 + 7 + PDS_DATA_DIRECTORY=/pds 8 + PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks 9 + 10 + PDS_DID_PLC_URL=http://localhost:8080 11 + PDS_HOSTNAME=pds.tngl.boltless.dev 12 + PDS_PORT=3000
+26
contrib/readme.md
··· 1 + # how to setup local appview dev environment 2 + 3 + Appview requires several microservices from knot and spindle to entire atproto infra. This test environment is implemented under nixos vm. 4 + 5 + 1. copy `contrib/example.env` to `.env`, fill it and source it 6 + 2. run vm 7 + ```bash 8 + nix run --impure .#vm 9 + ``` 10 + 3. copy generated `root.crt` file to `contrib/certs/root.crt` 11 + 4. trust the generated cert from host machine 12 + ```bash 13 + # for macos 14 + sudo security add-trusted-cert -d -r trustRoot \ 15 + -k /Library/Keychains/System.keychain \ 16 + ./nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/root.crt 17 + ``` 18 + 5. create test accounts with valid emails (use [`create-test-account.sh`](./scripts/create-test-account.sh)) 19 + 6. create default labels (use [`setup-const-records`](./scripts/setup-const-records.sh)) 20 + 7. restart vm with correct owner-did 21 + 22 + for git-https, you should change your local git config: 23 + ``` 24 + [http "https://knot.tngl.boltless.dev"] 25 + sslCAPath = /Users/boltless/repo/tangled/nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/ 26 + ```
+68
contrib/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 + # PDS_HOSTNAME= 9 + # PDS_ADMIN_PASSWORD= 10 + 11 + # curl a URL and fail if the request fails. 12 + function curl_cmd_get { 13 + curl --fail --silent --show-error "$@" 14 + } 15 + 16 + # curl a URL and fail if the request fails. 17 + function curl_cmd_post { 18 + curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@" 19 + } 20 + 21 + # curl a URL but do not fail if the request fails. 22 + function curl_cmd_post_nofail { 23 + curl --silent --show-error --request POST --header "Content-Type: application/json" "$@" 24 + } 25 + 26 + USERNAME="${1:-}" 27 + 28 + if [[ "${USERNAME}" == "" ]]; then 29 + read -p "Enter a username: " USERNAME 30 + fi 31 + 32 + if [[ "${USERNAME}" == "" ]]; then 33 + echo "ERROR: missing USERNAME parameter." >/dev/stderr 34 + echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr 35 + exit 1 36 + fi 37 + 38 + EMAIL=${USERNAME}@${PDS_HOSTNAME} 39 + 40 + PASSWORD="password" 41 + INVITE_CODE="$(curl_cmd_post \ 42 + --user "admin:${PDS_ADMIN_PASSWORD}" \ 43 + --data '{"useCount": 1}' \ 44 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code' 45 + )" 46 + RESULT="$(curl_cmd_post_nofail \ 47 + --data "{\"email\":\"${EMAIL}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \ 48 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount" 49 + )" 50 + 51 + DID="$(echo $RESULT | jq --raw-output '.did')" 52 + if [[ "${DID}" != did:* ]]; then 53 + ERR="$(echo ${RESULT} | jq --raw-output '.message')" 54 + echo "ERROR: ${ERR}" >/dev/stderr 55 + echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr 56 + exit 1 57 + fi 58 + 59 + echo 60 + echo "Account created successfully!" 61 + echo "-----------------------------" 62 + echo "Handle : ${USERNAME}.${PDS_HOSTNAME}" 63 + echo "DID : ${DID}" 64 + echo "Password : ${PASSWORD}" 65 + echo "-----------------------------" 66 + echo "This is a test account with an insecure password." 67 + echo "Make sure it's only used for development." 68 + echo
+106
contrib/scripts/setup-const-records.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 + # PDS_HOSTNAME= 9 + 10 + # curl a URL and fail if the request fails. 11 + function curl_cmd_get { 12 + curl --fail --silent --show-error "$@" 13 + } 14 + 15 + # curl a URL and fail if the request fails. 16 + function curl_cmd_post { 17 + curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@" 18 + } 19 + 20 + # curl a URL but do not fail if the request fails. 21 + function curl_cmd_post_nofail { 22 + curl --silent --show-error --request POST --header "Content-Type: application/json" "$@" 23 + } 24 + 25 + USERNAME="${1:-}" 26 + 27 + if [[ "${USERNAME}" == "" ]]; then 28 + read -p "Enter a username: " USERNAME 29 + fi 30 + 31 + if [[ "${USERNAME}" == "" ]]; then 32 + echo "ERROR: missing USERNAME parameter." >/dev/stderr 33 + echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr 34 + exit 1 35 + fi 36 + 37 + SESS_RESULT="$(curl_cmd_post \ 38 + --data "$(cat <<EOF 39 + { 40 + "identifier": "$USERNAME", 41 + "password": "password" 42 + } 43 + EOF 44 + )" \ 45 + https://pds.tngl.boltless.dev/xrpc/com.atproto.server.createSession 46 + )" 47 + 48 + echo $SESS_RESULT | jq 49 + 50 + DID="$(echo $SESS_RESULT | jq --raw-output '.did')" 51 + ACCESS_JWT="$(echo $SESS_RESULT | jq --raw-output '.accessJwt')" 52 + 53 + function add_label_def { 54 + local color=$1 55 + local name=$2 56 + echo $color 57 + echo $name 58 + local json_payload=$(cat <<EOF 59 + { 60 + "repo": "$DID", 61 + "collection": "sh.tangled.label.definition", 62 + "rkey": "$name", 63 + "record": { 64 + "name": "$name", 65 + "color": "$color", 66 + "scope": ["sh.tangled.repo.issue"], 67 + "multiple": false, 68 + "createdAt": "2025-09-22T11:14:35+01:00", 69 + "valueType": {"type": "null", "format": "any"} 70 + } 71 + } 72 + EOF 73 + ) 74 + echo $json_payload 75 + echo $json_payload | jq 76 + RESULT="$(curl_cmd_post \ 77 + --data "$json_payload" \ 78 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 79 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord")" 80 + echo $RESULT | jq 81 + } 82 + 83 + add_label_def '#64748b' 'wontfix' 84 + add_label_def '#8B5CF6' 'good-first-issue' 85 + add_label_def '#ef4444' 'duplicate' 86 + add_label_def '#06b6d4' 'documentation' 87 + json_payload=$(cat <<EOF 88 + { 89 + "repo": "$DID", 90 + "collection": "sh.tangled.label.definition", 91 + "rkey": "assignee", 92 + "record": { 93 + "name": "assignee", 94 + "color": "#10B981", 95 + "scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"], 96 + "multiple": false, 97 + "createdAt": "2025-09-22T11:14:35+01:00", 98 + "valueType": {"type": "string", "format": "did"} 99 + } 100 + } 101 + EOF 102 + ) 103 + curl_cmd_post \ 104 + --data "$json_payload" \ 105 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 106 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord"
+1 -1
flake.nix
··· 302 302 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 303 303 cd "$rootDir" 304 304 305 - mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs} 305 + mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs} 306 306 307 307 export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 308 308 exec ${pkgs.lib.getExe
+1 -1
knotmirror/knotstream/slurper.go
··· 122 122 } 123 123 124 124 // if this isn't a localhost / private connection, then we should enable SSRF protections 125 - if !host.NoSSL { 125 + if !host.NoSSL && false { 126 126 netDialer := ssrf.PublicOnlyDialer() 127 127 dialer.NetDialContext = netDialer.DialContext 128 128 }
+1
nix/modules/knotmirror.nix
··· 135 135 StateDirectory = "knotmirror"; 136 136 Environment = [ 137 137 # TODO: add environment variables 138 + "MIRROR_PLC_URL=${cfg.atpPlcUrl}" 138 139 "MIRROR_LISTEN=${cfg.listenAddr}" 139 140 "MIRROR_HOSTNAME=${cfg.hostname}" 140 141 "MIRROR_TAP_URL=http://localhost:${toString cfg.tap.port}"
+130
nix/vm.nix
··· 18 18 else default; 19 19 20 20 plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory"; 21 + relayUrl = envVarOr "TANGLED_VM_RELAY_URL" "https://relay1.us-east.bsky.network"; 21 22 jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe"; 22 23 in 23 24 nixpkgs.lib.nixosSystem { 24 25 inherit system; 25 26 modules = [ 27 + self.nixosModules.did-method-plc 28 + self.nixosModules.bluesky-jetstream 29 + self.nixosModules.bluesky-relay 26 30 self.nixosModules.knot 27 31 self.nixosModules.spindle 28 32 self.nixosModules.knotmirror ··· 40 44 diskSize = 10 * 1024; 41 45 cores = 2; 42 46 forwardPorts = [ 47 + # caddy 48 + { 49 + from = "host"; 50 + host.port = 80; 51 + guest.port = 80; 52 + } 53 + { 54 + from = "host"; 55 + host.port = 443; 56 + guest.port = 443; 57 + } 58 + { 59 + from = "host"; 60 + proto = "udp"; 61 + host.port = 443; 62 + guest.port = 443; 63 + } 43 64 # ssh 44 65 { 45 66 from = "host"; ··· 82 103 # as SQLite is incompatible with them. So instead we 83 104 # mount the shared directories to a different location 84 105 # and copy the contents around on service start/stop. 106 + caddyData = { 107 + source = "$TANGLED_VM_DATA_DIR/caddy"; 108 + target = config.services.caddy.dataDir; 109 + }; 85 110 knotData = { 86 111 source = "$TANGLED_VM_DATA_DIR/knot"; 87 112 target = "/mnt/knot-data"; ··· 98 123 }; 99 124 # 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 100 125 networking.firewall.enable = false; 126 + # resolve `*.tngl.boltless.dev` to host 127 + services.dnsmasq.enable = true; 128 + services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2"; 129 + security.pki.certificates = [ 130 + (builtins.readFile ../contrib/certs/root.crt) 131 + ]; 101 132 time.timeZone = "Europe/London"; 133 + services.timesyncd.enable = lib.mkVMOverride true; 102 134 services.getty.autologinUser = "root"; 103 135 environment.systemPackages = with pkgs; [curl vim git sqlite litecli postgresql_14]; 136 + virtualisation.docker.extraOptions = '' 137 + --dns 172.17.0.1 138 + ''; 104 139 services.tangled.knot = { 105 140 enable = true; 106 141 motd = "Welcome to the development knot!\n"; ··· 157 192 dbUrl = "postgresql://tnglr@127.0.0.1:5432/mirror"; 158 193 fullNetwork = false; 159 194 tap.dbUrl = "postgresql://tnglr@127.0.0.1:5432/tap"; 195 + atpPlcUrl = plcUrl; 196 + atpRelayUrl = relayUrl; 197 + }; 198 + services.did-method-plc.enable = true; 199 + services.bluesky-pds = { 200 + enable = true; 201 + # overriding package version to support emails 202 + package = pkgs.bluesky-pds.overrideAttrs (old: rec { 203 + version = "0.4.188"; 204 + src = pkgs.fetchFromGitHub { 205 + owner = "bluesky-social"; 206 + repo = "pds"; 207 + tag = "v${version}"; 208 + hash = "sha256-t8KdyEygXdbj/5Rhj8W40e1o8mXprELpjsKddHExmo0="; 209 + }; 210 + pnpmDeps = pkgs.fetchPnpmDeps { 211 + inherit version src; 212 + pname = old.pname; 213 + sourceRoot = old.sourceRoot; 214 + fetcherVersion = 2; 215 + hash = "sha256-lQie7f8JbWKSpoavnMjHegBzH3GB9teXsn+S2SLJHHU="; 216 + }; 217 + }); 218 + settings = { 219 + LOG_ENABLED = "true"; 220 + 221 + PDS_JWT_SECRET = "8cae8bffcc73d9932819650791e4e89a"; 222 + PDS_ADMIN_PASSWORD = "d6a902588cd93bee1af83f924f60cfd3"; 223 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = "2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7"; 224 + 225 + PDS_EMAIL_SMTP_URL = envVarOr "TANGLED_VM_PDS_EMAIL_SMTP_URL" null; 226 + PDS_EMAIL_FROM_ADDRESS = envVarOr "TANGLED_VM_PDS_EMAIL_FROM_ADDRESS" null; 227 + 228 + PDS_DID_PLC_URL = "http://localhost:8080"; 229 + PDS_CRAWLERS = "https://relay.tngl.boltless.dev"; 230 + PDS_HOSTNAME = "pds.tngl.boltless.dev"; 231 + PDS_PORT = 3000; 232 + }; 233 + }; 234 + services.bluesky-relay = { 235 + enable = true; 236 + }; 237 + services.bluesky-jetstream = { 238 + enable = true; 239 + livenessTtl = 300; 240 + websocketUrl = "ws://localhost:3000/xrpc/com.atproto.sync.subscribeRepos"; 241 + }; 242 + services.caddy = { 243 + enable = true; 244 + configFile = pkgs.writeText "Caddyfile" '' 245 + { 246 + debug 247 + cert_lifetime 3601d 248 + pki { 249 + ca local { 250 + intermediate_lifetime 3599d 251 + } 252 + } 253 + } 254 + 255 + plc.tngl.boltless.dev { 256 + tls internal 257 + reverse_proxy http://localhost:8080 258 + } 259 + 260 + *.pds.tngl.boltless.dev, pds.tngl.boltless.dev { 261 + tls internal 262 + reverse_proxy http://localhost:3000 263 + } 264 + 265 + jetstream.tngl.boltless.dev { 266 + tls internal 267 + reverse_proxy http://localhost:6008 268 + } 269 + 270 + relay.tngl.boltless.dev { 271 + tls internal 272 + reverse_proxy http://localhost:2470 273 + } 274 + 275 + knot.tngl.boltless.dev { 276 + tls internal 277 + reverse_proxy http://localhost:6444 278 + } 279 + 280 + spindle.tngl.boltless.dev { 281 + tls internal 282 + reverse_proxy http://localhost:6555 283 + } 284 + 285 + mirror.tngl.boltless.dev { 286 + tls internal 287 + reverse_proxy http://localhost:7000 288 + } 289 + ''; 160 290 }; 161 291 users = { 162 292 # So we don't have to deal with permission clashing between