forked from tangled.org/core
Monorepo for Tangled

local-infra: local, sandboxed atmosphere infra

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

boltless.me c1df9b8e ca41f242

verified
+54
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 + https://knot.tngl.boltless.dev { 44 + tls { 45 + issuer internal { 46 + ca localtangled 47 + } 48 + } 49 + reverse_proxy http://host.docker.internal:6000 50 + } 51 + 52 + http://spindle.tngl.boltless.dev { 53 + reverse_proxy http://host.docker.internal:6555 54 + }
+12
local-infra/cert/localtangled/intermediate.crt
···
··· 1 + -----BEGIN CERTIFICATE----- 2 + MIIBuTCCAV+gAwIBAgIQR5mkZ/TBSWtRFqrMyeVrNDAKBggqhkjOPQQDAjApMScw 3 + JQYDVQQDEx5Mb2NhbFRhbmdsZWRDQSAtIDIwMjUgRUNDIFJvb3QwHhcNMjUxMTAz 4 + MTAyMDA0WhcNMjUxMTEwMTAyMDA0WjAsMSowKAYDVQQDEyFMb2NhbFRhbmdsZWRD 5 + QSAtIEVDQyBJbnRlcm1lZGlhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARm 6 + 892T608pFY+dmgkEMFdvq9hj+PlR7o7Vogc+Ca5LeHB846PrZJmxdvHW8Up67hP3 7 + ZpmNjnZQvgOEEjLmquvio2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgw 8 + BgEB/wIBADAdBgNVHQ4EFgQUn8d4TdPCYP0r1Jc09QF4/GKkSSowHwYDVR0jBBgw 9 + FoAUKSXx08/YgAxM+u7pYQcs/WHJIRAwCgYIKoZIzj0EAwIDSAAwRQIgTZeKVo6k 10 + ZBZwx2sx+T46LyjYc5xK/DCQJbLWsgoc/lECIQDNtduyds5J/BfBvnVzO/oK9+0H 11 + oRvV+fcWRAQHGKF4Ew== 12 + -----END CERTIFICATE-----
+5
local-infra/cert/localtangled/intermediate.key
···
··· 1 + -----BEGIN EC PRIVATE KEY----- 2 + MHcCAQEEIOoepsyQeMkbA05rTh3EwvqHWs5tzTTib7r8fyt2fUo8oAoGCCqGSM49 3 + AwEHoUQDQgAEZvPdk+tPKRWPnZoJBDBXb6vYY/j5Ue6O1aIHPgmuS3hwfOOj62SZ 4 + sXbx1vFKeu4T92aZjY52UL4DhBIy5qrr4g== 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-----
+75
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 + jetstream: 59 + container_name: jetstream 60 + image: ghcr.io/bluesky-social/jetstream:sha-0ab10bd 61 + restart: unless-stopped 62 + volumes: 63 + - jetstream:/data 64 + environment: 65 + - JETSTREAM_DATA_DIR=/data 66 + # livness check interval to restart when no events are received (default: 15sec) 67 + - JETSTREAM_LIVENESS_TTL=300s 68 + - JETSTREAM_WS_URL=ws://pds:3000/xrpc/com.atproto.sync.subscribeRepos 69 + 70 + volumes: 71 + caddy_config: 72 + caddy_data: 73 + plc: 74 + pds: 75 + 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 + ```
+65
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 + EMAIL=${USERNAME}@${PDS_HOSTNAME} 36 + 37 + PASSWORD="password" 38 + INVITE_CODE="$(curl_cmd_post \ 39 + --user "admin:${PDS_ADMIN_PASSWORD}" \ 40 + --data '{"useCount": 1}' \ 41 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code' 42 + )" 43 + RESULT="$(curl_cmd_post_nofail \ 44 + --data "{\"email\":\"${EMAIL}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \ 45 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount" 46 + )" 47 + 48 + DID="$(echo $RESULT | jq --raw-output '.did')" 49 + if [[ "${DID}" != did:* ]]; then 50 + ERR="$(echo ${RESULT} | jq --raw-output '.message')" 51 + echo "ERROR: ${ERR}" >/dev/stderr 52 + echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr 53 + exit 1 54 + fi 55 + 56 + echo 57 + echo "Account created successfully!" 58 + echo "-----------------------------" 59 + echo "Handle : ${USERNAME}.${PDS_HOSTNAME}" 60 + echo "DID : ${DID}" 61 + echo "Password : ${PASSWORD}" 62 + echo "-----------------------------" 63 + echo "This is a test account with an insecure password." 64 + echo "Make sure it's only used for development." 65 + echo
+104
local-infra/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 + # 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 + SESS_RESULT="$(curl_cmd_post \ 36 + --data "$(cat <<EOF 37 + { 38 + "identifier": "$USERNAME", 39 + "password": "password" 40 + } 41 + EOF 42 + )" \ 43 + https://pds.tngl.boltless.dev/xrpc/com.atproto.server.createSession 44 + )" 45 + 46 + echo $SESS_RESULT | jq 47 + 48 + DID="$(echo $SESS_RESULT | jq --raw-output '.did')" 49 + ACCESS_JWT="$(echo $SESS_RESULT | jq --raw-output '.accessJwt')" 50 + 51 + function add_label_def { 52 + local color=$1 53 + local name=$2 54 + echo $color 55 + echo $name 56 + local json_payload=$(cat <<EOF 57 + { 58 + "repo": "$DID", 59 + "collection": "sh.tangled.label.definition", 60 + "rkey": "$name", 61 + "record": { 62 + "name": "$name", 63 + "color": "$color", 64 + "scope": ["sh.tangled.repo.issue"], 65 + "multiple": false, 66 + "createdAt": "2025-09-22T11:14:35+01:00", 67 + "valueType": {"type": "null", "format": "any"} 68 + } 69 + } 70 + EOF 71 + ) 72 + echo $json_payload 73 + echo $json_payload | jq 74 + RESULT="$(curl_cmd_post \ 75 + --data "$json_payload" \ 76 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 77 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord")" 78 + echo $RESULT | jq 79 + } 80 + 81 + add_label_def '#64748b' 'wontfix' 82 + add_label_def '#8B5CF6' 'good-first-issue' 83 + add_label_def '#ef4444' 'duplicate' 84 + add_label_def '#06b6d4' 'documentation' 85 + json_payload=$(cat <<EOF 86 + { 87 + "repo": "$DID", 88 + "collection": "sh.tangled.label.definition", 89 + "rkey": "assignee", 90 + "record": { 91 + "name": "assignee", 92 + "color": "#10B981", 93 + "scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"], 94 + "multiple": false, 95 + "createdAt": "2025-09-22T11:14:35+01:00", 96 + "valueType": {"type": "string", "format": "did"} 97 + } 98 + } 99 + EOF 100 + ) 101 + curl_cmd_post \ 102 + --data "$json_payload" \ 103 + -H "Authorization: Bearer ${ACCESS_JWT}" \ 104 + "https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord"
+5
nix/vm.nix
··· 79 }; 80 # 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 81 networking.firewall.enable = false; 82 time.timeZone = "Europe/London"; 83 services.getty.autologinUser = "root"; 84 environment.systemPackages = with pkgs; [curl vim git sqlite litecli];
··· 79 }; 80 # 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 81 networking.firewall.enable = false; 82 + services.dnsmasq.enable = true; 83 + services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2"; 84 + security.pki.certificates = [ 85 + (builtins.readFile ../local-infra/cert/localtangled/root.crt) 86 + ]; 87 time.timeZone = "Europe/London"; 88 services.getty.autologinUser = "root"; 89 environment.systemPackages = with pkgs; [curl vim git sqlite litecli];