this repo has no description

Rename to tranquil PDS, sounds better than bullshit PDS

lewis a4d6d0fd 920994c7

+2 -2
.env.example
··· 30 # Security Secrets 31 # ============================================================================= 32 # These MUST be set in production (minimum 32 characters each) 33 - # In development, set BSPDS_ALLOW_INSECURE_SECRETS=1 to use defaults 34 # Server-wide secret for OAuth token signing (HS256) 35 # JWT_SECRET=your-secure-random-string-at-least-32-chars 36 # Secret for DPoP proof validation ··· 38 # Key for encrypting user signing keys at rest (AES-256-GCM) 39 # MASTER_KEY=your-secure-random-string-at-least-32-chars 40 # Set this ONLY in development to allow default/weak secrets 41 - # BSPDS_ALLOW_INSECURE_SECRETS=1 42 # ============================================================================= 43 # PLC Directory 44 # =============================================================================
··· 30 # Security Secrets 31 # ============================================================================= 32 # These MUST be set in production (minimum 32 characters each) 33 + # In development, set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 to use defaults 34 # Server-wide secret for OAuth token signing (HS256) 35 # JWT_SECRET=your-secure-random-string-at-least-32-chars 36 # Secret for DPoP proof validation ··· 38 # Key for encrypting user signing keys at rest (AES-256-GCM) 39 # MASTER_KEY=your-secure-random-string-at-least-32-chars 40 # Set this ONLY in development to allow default/weak secrets 41 + # TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 42 # ============================================================================= 43 # PLC Directory 44 # =============================================================================
+22
.sqlx/query-1add22e111d5eff8beadbd832b4b8146d95da0a0ce8ce31dc9a2f930a26cc9ce.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT takedown_ref FROM users WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "takedown_ref", 9 + "type_info": "Text" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + true 19 + ] 20 + }, 21 + "hash": "1add22e111d5eff8beadbd832b4b8146d95da0a0ce8ce31dc9a2f930a26cc9ce" 22 + }
+61 -61
Cargo.lock
··· 930 ] 931 932 [[package]] 933 - name = "bspds" 934 - version = "0.1.0" 935 - dependencies = [ 936 - "aes-gcm", 937 - "anyhow", 938 - "async-trait", 939 - "aws-config", 940 - "aws-sdk-s3", 941 - "axum", 942 - "base32", 943 - "base64 0.22.1", 944 - "bcrypt", 945 - "bytes", 946 - "chrono", 947 - "cid", 948 - "ctor", 949 - "dotenvy", 950 - "ed25519-dalek", 951 - "futures", 952 - "governor", 953 - "hickory-resolver", 954 - "hkdf", 955 - "hmac", 956 - "image", 957 - "ipld-core", 958 - "iroh-car", 959 - "jacquard", 960 - "jacquard-axum", 961 - "jacquard-repo", 962 - "jsonwebtoken", 963 - "k256", 964 - "metrics", 965 - "metrics-exporter-prometheus", 966 - "multibase", 967 - "multihash", 968 - "p256 0.13.2", 969 - "p384", 970 - "rand 0.8.5", 971 - "redis", 972 - "reqwest", 973 - "serde", 974 - "serde_bytes", 975 - "serde_ipld_dagcbor", 976 - "serde_json", 977 - "sha2", 978 - "sqlx", 979 - "subtle", 980 - "testcontainers", 981 - "testcontainers-modules", 982 - "thiserror 2.0.17", 983 - "tokio", 984 - "tokio-tungstenite", 985 - "tower-http", 986 - "tracing", 987 - "tracing-subscriber", 988 - "urlencoding", 989 - "uuid", 990 - "wiremock", 991 - ] 992 - 993 - [[package]] 994 name = "btree-range-map" 995 version = "0.7.2" 996 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6221 "proc-macro2", 6222 "quote", 6223 "syn 2.0.111", 6224 ] 6225 6226 [[package]]
··· 930 ] 931 932 [[package]] 933 name = "btree-range-map" 934 version = "0.7.2" 935 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6160 "proc-macro2", 6161 "quote", 6162 "syn 2.0.111", 6163 + ] 6164 + 6165 + [[package]] 6166 + name = "tranquil-pds" 6167 + version = "0.1.0" 6168 + dependencies = [ 6169 + "aes-gcm", 6170 + "anyhow", 6171 + "async-trait", 6172 + "aws-config", 6173 + "aws-sdk-s3", 6174 + "axum", 6175 + "base32", 6176 + "base64 0.22.1", 6177 + "bcrypt", 6178 + "bytes", 6179 + "chrono", 6180 + "cid", 6181 + "ctor", 6182 + "dotenvy", 6183 + "ed25519-dalek", 6184 + "futures", 6185 + "governor", 6186 + "hickory-resolver", 6187 + "hkdf", 6188 + "hmac", 6189 + "image", 6190 + "ipld-core", 6191 + "iroh-car", 6192 + "jacquard", 6193 + "jacquard-axum", 6194 + "jacquard-repo", 6195 + "jsonwebtoken", 6196 + "k256", 6197 + "metrics", 6198 + "metrics-exporter-prometheus", 6199 + "multibase", 6200 + "multihash", 6201 + "p256 0.13.2", 6202 + "p384", 6203 + "rand 0.8.5", 6204 + "redis", 6205 + "reqwest", 6206 + "serde", 6207 + "serde_bytes", 6208 + "serde_ipld_dagcbor", 6209 + "serde_json", 6210 + "sha2", 6211 + "sqlx", 6212 + "subtle", 6213 + "testcontainers", 6214 + "testcontainers-modules", 6215 + "thiserror 2.0.17", 6216 + "tokio", 6217 + "tokio-tungstenite", 6218 + "tower-http", 6219 + "tracing", 6220 + "tracing-subscriber", 6221 + "urlencoding", 6222 + "uuid", 6223 + "wiremock", 6224 ] 6225 6226 [[package]]
+1 -1
Cargo.toml
··· 1 [package] 2 - name = "bspds" 3 version = "0.1.0" 4 edition = "2024" 5 [dependencies]
··· 1 [package] 2 + name = "tranquil-pds" 3 version = "0.1.0" 4 edition = "2024" 5 [dependencies]
+2 -2
Dockerfile
··· 16 RUN touch src/main.rs && cargo build --release 17 # Stage 3: Final image 18 FROM alpine:3.23 19 - COPY --from=builder /app/target/release/bspds /usr/local/bin/bspds 20 COPY --from=builder /app/migrations /app/migrations 21 COPY --from=frontend-builder /frontend/dist /app/frontend/dist 22 WORKDIR /app ··· 24 ENV SERVER_PORT=3000 25 ENV FRONTEND_DIR=/app/frontend/dist 26 EXPOSE 3000 27 - CMD ["bspds"]
··· 16 RUN touch src/main.rs && cargo build --release 17 # Stage 3: Final image 18 FROM alpine:3.23 19 + COPY --from=builder /app/target/release/tranquil-pds /usr/local/bin/tranquil-pds 20 COPY --from=builder /app/migrations /app/migrations 21 COPY --from=frontend-builder /frontend/dist /app/frontend/dist 22 WORKDIR /app ··· 24 ENV SERVER_PORT=3000 25 ENV FRONTEND_DIR=/app/frontend/dist 26 EXPOSE 3000 27 + CMD ["tranquil-pds"]
+1 -1
README.md
··· 1 - # BSPDS 2 3 A production-grade Personal Data Server (PDS) for the AT Protocol. Drop-in replacement for Bluesky's reference PDS, written in rust with postgres and s3-compatible blob storage. 4
··· 1 + # Tranquil PDS 2 3 A production-grade Personal Data Server (PDS) for the AT Protocol. Drop-in replacement for Bluesky's reference PDS, written in rust with postgres and s3-compatible blob storage. 4
+6 -6
deploy/quadlets/bspds-app.container deploy/quadlets/tranquil-pds-app.container
··· 1 [Unit] 2 - Description=BSPDS AT Protocol PDS 3 - After=bspds-db.service bspds-minio.service bspds-valkey.service 4 [Container] 5 - ContainerName=bspds-app 6 - Image=localhost/bspds:latest 7 - Pod=bspds.pod 8 - EnvironmentFile=/srv/bspds/config/bspds.env 9 Environment=SERVER_HOST=0.0.0.0 10 Environment=SERVER_PORT=3000 11 Environment=S3_ENDPOINT=http://localhost:9000
··· 1 [Unit] 2 + Description=Tranquil PDS AT Protocol PDS 3 + After=tranquil-pds-db.service tranquil-pds-minio.service tranquil-pds-valkey.service 4 [Container] 5 + ContainerName=tranquil-pds-app 6 + Image=localhost/tranquil-pds:latest 7 + Pod=tranquil-pds.pod 8 + EnvironmentFile=/srv/tranquil-pds/config/tranquil-pds.env 9 Environment=SERVER_HOST=0.0.0.0 10 Environment=SERVER_PORT=3000 11 Environment=S3_ENDPOINT=http://localhost:9000
-20
deploy/quadlets/bspds-db.container
··· 1 - [Unit] 2 - Description=BSPDS postgres database 3 - [Container] 4 - ContainerName=bspds-db 5 - Image=docker.io/library/postgres:18-alpine 6 - Pod=bspds.pod 7 - Environment=POSTGRES_USER=bspds 8 - Environment=POSTGRES_DB=pds 9 - Secret=bspds-db-password,type=env,target=POSTGRES_PASSWORD 10 - Volume=/srv/bspds/postgres:/var/lib/postgresql/data:Z 11 - HealthCmd=pg_isready -U bspds -d pds 12 - HealthInterval=10s 13 - HealthTimeout=5s 14 - HealthRetries=5 15 - HealthStartPeriod=10s 16 - [Service] 17 - Restart=always 18 - RestartSec=10 19 - [Install] 20 - WantedBy=default.target
···
+5 -5
deploy/quadlets/bspds-minio.container deploy/quadlets/tranquil-pds-minio.container
··· 1 [Unit] 2 - Description=BSPDS minio object storage 3 [Container] 4 - ContainerName=bspds-minio 5 Image=docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z 6 - Pod=bspds.pod 7 Environment=MINIO_ROOT_USER=minioadmin 8 - Secret=bspds-minio-password,type=env,target=MINIO_ROOT_PASSWORD 9 - Volume=/srv/bspds/minio:/data:Z 10 Exec=server /data --console-address :9001 11 HealthCmd=curl -f http://localhost:9000/minio/health/live || exit 1 12 HealthInterval=30s
··· 1 [Unit] 2 + Description=Tranquil PDS minio object storage 3 [Container] 4 + ContainerName=tranquil-pds-minio 5 Image=docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z 6 + Pod=tranquil-pds.pod 7 Environment=MINIO_ROOT_USER=minioadmin 8 + Secret=tranquil-pds-minio-password,type=env,target=MINIO_ROOT_PASSWORD 9 + Volume=/srv/tranquil-pds/minio:/data:Z 10 Exec=server /data --console-address :9001 11 HealthCmd=curl -f http://localhost:9000/minio/health/live || exit 1 12 HealthInterval=30s
-15
deploy/quadlets/bspds-nginx.container
··· 1 - [Unit] 2 - Description=BSPDS nginx reverse proxy 3 - After=bspds-app.service 4 - [Container] 5 - ContainerName=bspds-nginx 6 - Image=docker.io/library/nginx:1.28-alpine 7 - Pod=bspds.pod 8 - Volume=/srv/bspds/config/nginx.conf:/etc/nginx/nginx.conf:ro,Z 9 - Volume=/srv/bspds/certs:/etc/nginx/certs:ro,Z 10 - Volume=/srv/bspds/acme:/var/www/acme:ro,Z 11 - [Service] 12 - Restart=always 13 - RestartSec=10 14 - [Install] 15 - WantedBy=default.target
···
+4 -4
deploy/quadlets/bspds-valkey.container deploy/quadlets/tranquil-pds-valkey.container
··· 1 [Unit] 2 - Description=BSPDS valkey cache 3 [Container] 4 - ContainerName=bspds-valkey 5 Image=docker.io/valkey/valkey:9-alpine 6 - Pod=bspds.pod 7 - Volume=/srv/bspds/valkey:/data:Z 8 Exec=valkey-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru 9 HealthCmd=valkey-cli ping 10 HealthInterval=10s
··· 1 [Unit] 2 + Description=Tranquil PDS valkey cache 3 [Container] 4 + ContainerName=tranquil-pds-valkey 5 Image=docker.io/valkey/valkey:9-alpine 6 + Pod=tranquil-pds.pod 7 + Volume=/srv/tranquil-pds/valkey:/data:Z 8 Exec=valkey-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru 9 HealthCmd=valkey-cli ping 10 HealthInterval=10s
+1 -1
deploy/quadlets/bspds.pod deploy/quadlets/tranquil-pds.pod
··· 1 [Pod] 2 - PodName=bspds 3 PublishPort=80:80 4 PublishPort=443:443 5 [Install]
··· 1 [Pod] 2 + PodName=tranquil-pds 3 PublishPort=80:80 4 PublishPort=443:443 5 [Install]
+20
deploy/quadlets/tranquil-pds-db.container
···
··· 1 + [Unit] 2 + Description=Tranquil PDS postgres database 3 + [Container] 4 + ContainerName=tranquil-pds-db 5 + Image=docker.io/library/postgres:18-alpine 6 + Pod=tranquil-pds.pod 7 + Environment=POSTGRES_USER=tranquil_pds 8 + Environment=POSTGRES_DB=pds 9 + Secret=tranquil-pds-db-password,type=env,target=POSTGRES_PASSWORD 10 + Volume=/srv/tranquil-pds/postgres:/var/lib/postgresql/data:Z 11 + HealthCmd=pg_isready -U tranquil_pds -d pds 12 + HealthInterval=10s 13 + HealthTimeout=5s 14 + HealthRetries=5 15 + HealthStartPeriod=10s 16 + [Service] 17 + Restart=always 18 + RestartSec=10 19 + [Install] 20 + WantedBy=default.target
+15
deploy/quadlets/tranquil-pds-nginx.container
···
··· 1 + [Unit] 2 + Description=Tranquil PDS nginx reverse proxy 3 + After=tranquil-pds-app.service 4 + [Container] 5 + ContainerName=tranquil-pds-nginx 6 + Image=docker.io/library/nginx:1.28-alpine 7 + Pod=tranquil-pds.pod 8 + Volume=/srv/tranquil-pds/config/nginx.conf:/etc/nginx/nginx.conf:ro,Z 9 + Volume=/srv/tranquil-pds/certs:/etc/nginx/certs:ro,Z 10 + Volume=/srv/tranquil-pds/acme:/var/www/acme:ro,Z 11 + [Service] 12 + Restart=always 13 + RestartSec=10 14 + [Install] 15 + WantedBy=default.target
+6 -6
docker-compose.prod.yml
··· 1 services: 2 - bspds: 3 build: 4 context: . 5 dockerfile: Dockerfile 6 - image: bspds:latest 7 restart: unless-stopped 8 ports: 9 - "127.0.0.1:3000:3000" ··· 11 SERVER_HOST: "0.0.0.0" 12 SERVER_PORT: "3000" 13 PDS_HOSTNAME: "${PDS_HOSTNAME:?PDS_HOSTNAME is required}" 14 - DATABASE_URL: "postgres://bspds:${DB_PASSWORD:?DB_PASSWORD is required}@db:5432/pds" 15 S3_ENDPOINT: "http://minio:9000" 16 AWS_REGION: "us-east-1" 17 S3_BUCKET: "pds-blobs" ··· 46 image: postgres:18-alpine 47 restart: unless-stopped 48 environment: 49 - POSTGRES_USER: bspds 50 POSTGRES_PASSWORD: "${DB_PASSWORD:?DB_PASSWORD is required}" 51 POSTGRES_DB: pds 52 volumes: 53 - postgres_data:/var/lib/postgresql/data 54 healthcheck: 55 - test: ["CMD-SHELL", "pg_isready -U bspds -d pds"] 56 interval: 10s 57 timeout: 5s 58 retries: 5 ··· 128 - ./certs:/etc/nginx/certs:ro 129 - acme_challenge:/var/www/acme:ro 130 depends_on: 131 - - bspds 132 healthcheck: 133 test: ["CMD", "nginx", "-t"] 134 interval: 30s
··· 1 services: 2 + tranquil-pds: 3 build: 4 context: . 5 dockerfile: Dockerfile 6 + image: tranquil-pds:latest 7 restart: unless-stopped 8 ports: 9 - "127.0.0.1:3000:3000" ··· 11 SERVER_HOST: "0.0.0.0" 12 SERVER_PORT: "3000" 13 PDS_HOSTNAME: "${PDS_HOSTNAME:?PDS_HOSTNAME is required}" 14 + DATABASE_URL: "postgres://tranquil_pds:${DB_PASSWORD:?DB_PASSWORD is required}@db:5432/pds" 15 S3_ENDPOINT: "http://minio:9000" 16 AWS_REGION: "us-east-1" 17 S3_BUCKET: "pds-blobs" ··· 46 image: postgres:18-alpine 47 restart: unless-stopped 48 environment: 49 + POSTGRES_USER: tranquil_pds 50 POSTGRES_PASSWORD: "${DB_PASSWORD:?DB_PASSWORD is required}" 51 POSTGRES_DB: pds 52 volumes: 53 - postgres_data:/var/lib/postgresql/data 54 healthcheck: 55 + test: ["CMD-SHELL", "pg_isready -U tranquil_pds -d pds"] 56 interval: 10s 57 timeout: 5s 58 retries: 5 ··· 128 - ./certs:/etc/nginx/certs:ro 129 - acme_challenge:/var/www/acme:ro 130 depends_on: 131 + - tranquil-pds 132 healthcheck: 133 test: ["CMD", "nginx", "-t"] 134 interval: 30s
+1 -1
docker-compose.yaml
··· 3 build: 4 context: . 5 dockerfile: Dockerfile 6 - image: bspds 7 ports: 8 - "3000:3000" 9 env_file:
··· 3 build: 4 context: . 5 dockerfile: Dockerfile 6 + image: tranquil-pds 7 ports: 8 - "3000:3000" 9 env_file:
+42 -45
docs/install-alpine.md
··· 1 - # BSPDS Production Installation on Alpine Linux 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 4 - This guide covers installing BSPDS on Alpine Linux 3.23 (current stable as of December 2025). 5 6 ## Prerequisites 7 - A VPS with at least 2GB RAM and 20GB disk ··· 20 source ~/.cargo/env 21 rustup default stable 22 ``` 23 - This installs the latest stable Rust (1.92+ as of December 2025). Alpine 3.23 also ships Rust 1.91 via `apk add rust cargo` if you prefer system packages. 24 ## 3. Install postgres 25 - Alpine 3.23 includes PostgreSQL 18: 26 ```sh 27 apk add postgresql postgresql-contrib 28 rc-update add postgresql 29 /etc/init.d/postgresql setup 30 rc-service postgresql start 31 - psql -U postgres -c "CREATE USER bspds WITH PASSWORD 'your-secure-password';" 32 - psql -U postgres -c "CREATE DATABASE pds OWNER bspds;" 33 - psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;" 34 ``` 35 ## 4. Install minio 36 ```sh ··· 78 mc mb local/pds-blobs 79 ``` 80 ## 5. Install valkey 81 - Alpine 3.23 includes Valkey 9: 82 ```sh 83 apk add valkey 84 rc-update add valkey ··· 90 export PATH="$HOME/.deno/bin:$PATH" 91 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile 92 ``` 93 - ## 7. Clone and Build BSPDS 94 ```sh 95 mkdir -p /opt && cd /opt 96 - git clone https://tangled.org/lewis.moe/bspds-sandbox bspds 97 - cd bspds 98 cd frontend 99 deno task build 100 cd .. ··· 103 ## 8. Install sqlx-cli and Run Migrations 104 ```sh 105 cargo install sqlx-cli --no-default-features --features postgres 106 - export DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" 107 sqlx migrate run 108 ``` 109 - ## 9. Configure BSPDS 110 ```sh 111 - mkdir -p /etc/bspds 112 - cp /opt/bspds/.env.example /etc/bspds/bspds.env 113 - chmod 600 /etc/bspds/bspds.env 114 ``` 115 - Edit `/etc/bspds/bspds.env` and fill in your values. Generate secrets with: 116 ```sh 117 openssl rand -base64 48 118 ``` 119 ## 10. Create OpenRC Service 120 ```sh 121 - adduser -D -H -s /sbin/nologin bspds 122 - cp /opt/bspds/target/release/bspds /usr/local/bin/ 123 - mkdir -p /var/lib/bspds 124 - cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend 125 - chown -R bspds:bspds /var/lib/bspds 126 - cat > /etc/init.d/bspds << 'EOF' 127 #!/sbin/openrc-run 128 - name="bspds" 129 - description="BSPDS - AT Protocol PDS" 130 - command="/usr/local/bin/bspds" 131 - command_user="bspds" 132 command_background=true 133 pidfile="/run/${RC_SVCNAME}.pid" 134 - output_log="/var/log/bspds.log" 135 - error_log="/var/log/bspds.log" 136 depend() { 137 need net postgresql minio 138 } 139 start_pre() { 140 - export FRONTEND_DIR=/var/lib/bspds/frontend 141 - . /etc/bspds/bspds.env 142 export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL 143 export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY 144 export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS 145 } 146 EOF 147 - chmod +x /etc/init.d/bspds 148 - rc-update add bspds 149 - rc-service bspds start 150 ``` 151 ## 11. Install and Configure nginx 152 - Alpine 3.23 includes nginx 1.28: 153 ```sh 154 apk add nginx certbot certbot-nginx 155 - cat > /etc/nginx/http.d/bspds.conf << 'EOF' 156 server { 157 listen 80; 158 listen [::]:80; ··· 217 ``` 218 ## 14. Verify Installation 219 ```sh 220 - rc-service bspds status 221 curl -s https://pds.example.com/xrpc/_health 222 curl -s https://pds.example.com/.well-known/atproto-did 223 ``` 224 ## Maintenance 225 View logs: 226 ```sh 227 - tail -f /var/log/bspds.log 228 ``` 229 - Update BSPDS: 230 ```sh 231 - cd /opt/bspds 232 git pull 233 cd frontend && deno task build && cd .. 234 cargo build --release 235 - rc-service bspds stop 236 - cp target/release/bspds /usr/local/bin/ 237 - cp -r frontend/dist /var/lib/bspds/frontend 238 - DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" sqlx migrate run 239 - rc-service bspds start 240 ``` 241 Backup database: 242 ```sh
··· 1 + # Tranquil PDS Production Installation on Alpine Linux 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 4 + This guide covers installing Tranquil PDS on Alpine Linux 3.23. 5 6 ## Prerequisites 7 - A VPS with at least 2GB RAM and 20GB disk ··· 20 source ~/.cargo/env 21 rustup default stable 22 ``` 23 + This installs the latest stable Rust. Alpine also ships Rust via `apk add rust cargo` if you prefer system packages. 24 ## 3. Install postgres 25 ```sh 26 apk add postgresql postgresql-contrib 27 rc-update add postgresql 28 /etc/init.d/postgresql setup 29 rc-service postgresql start 30 + psql -U postgres -c "CREATE USER tranquil_pds WITH PASSWORD 'your-secure-password';" 31 + psql -U postgres -c "CREATE DATABASE pds OWNER tranquil_pds;" 32 + psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 33 ``` 34 ## 4. Install minio 35 ```sh ··· 77 mc mb local/pds-blobs 78 ``` 79 ## 5. Install valkey 80 ```sh 81 apk add valkey 82 rc-update add valkey ··· 88 export PATH="$HOME/.deno/bin:$PATH" 89 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile 90 ``` 91 + ## 7. Clone and Build Tranquil PDS 92 ```sh 93 mkdir -p /opt && cd /opt 94 + git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 95 + cd tranquil-pds 96 cd frontend 97 deno task build 98 cd .. ··· 101 ## 8. Install sqlx-cli and Run Migrations 102 ```sh 103 cargo install sqlx-cli --no-default-features --features postgres 104 + export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 105 sqlx migrate run 106 ``` 107 + ## 9. Configure Tranquil PDS 108 ```sh 109 + mkdir -p /etc/tranquil-pds 110 + cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env 111 + chmod 600 /etc/tranquil-pds/tranquil-pds.env 112 ``` 113 + Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with: 114 ```sh 115 openssl rand -base64 48 116 ``` 117 ## 10. Create OpenRC Service 118 ```sh 119 + adduser -D -H -s /sbin/nologin tranquil-pds 120 + cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 121 + mkdir -p /var/lib/tranquil-pds 122 + cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend 123 + chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds 124 + cat > /etc/init.d/tranquil-pds << 'EOF' 125 #!/sbin/openrc-run 126 + name="tranquil-pds" 127 + description="Tranquil PDS - AT Protocol PDS" 128 + command="/usr/local/bin/tranquil-pds" 129 + command_user="tranquil-pds" 130 command_background=true 131 pidfile="/run/${RC_SVCNAME}.pid" 132 + output_log="/var/log/tranquil-pds.log" 133 + error_log="/var/log/tranquil-pds.log" 134 depend() { 135 need net postgresql minio 136 } 137 start_pre() { 138 + export FRONTEND_DIR=/var/lib/tranquil-pds/frontend 139 + . /etc/tranquil-pds/tranquil-pds.env 140 export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL 141 export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY 142 export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS 143 } 144 EOF 145 + chmod +x /etc/init.d/tranquil-pds 146 + rc-update add tranquil-pds 147 + rc-service tranquil-pds start 148 ``` 149 ## 11. Install and Configure nginx 150 ```sh 151 apk add nginx certbot certbot-nginx 152 + cat > /etc/nginx/http.d/tranquil-pds.conf << 'EOF' 153 server { 154 listen 80; 155 listen [::]:80; ··· 214 ``` 215 ## 14. Verify Installation 216 ```sh 217 + rc-service tranquil-pds status 218 curl -s https://pds.example.com/xrpc/_health 219 curl -s https://pds.example.com/.well-known/atproto-did 220 ``` 221 ## Maintenance 222 View logs: 223 ```sh 224 + tail -f /var/log/tranquil-pds.log 225 ``` 226 + Update Tranquil PDS: 227 ```sh 228 + cd /opt/tranquil-pds 229 git pull 230 cd frontend && deno task build && cd .. 231 cargo build --release 232 + rc-service tranquil-pds stop 233 + cp target/release/tranquil-pds /usr/local/bin/ 234 + cp -r frontend/dist /var/lib/tranquil-pds/frontend 235 + DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 236 + rc-service tranquil-pds start 237 ``` 238 Backup database: 239 ```sh
+77 -77
docs/install-containers.md
··· 1 - # BSPDS Containerized Production Deployment 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 - This guide covers deploying BSPDS using containers with podman. 4 - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) 5 - **Alpine 3.23+**: Uses OpenRC service script with podman-compose 6 ## Prerequisites ··· 39 ## 2. Create Directory Structure 40 ```bash 41 mkdir -p /etc/containers/systemd 42 - mkdir -p /srv/bspds/{postgres,minio,valkey,certs,acme,config} 43 ``` 44 ## 3. Create Environment File 45 ```bash 46 - cp /opt/bspds/.env.example /srv/bspds/config/bspds.env 47 - chmod 600 /srv/bspds/config/bspds.env 48 ``` 49 - Edit `/srv/bspds/config/bspds.env` and fill in your values. Generate secrets with: 50 ```bash 51 openssl rand -base64 48 52 ``` ··· 54 ## 4. Install Quadlet Definitions 55 Copy the quadlet files from the repository: 56 ```bash 57 - cp /opt/bspds/deploy/quadlets/*.pod /etc/containers/systemd/ 58 - cp /opt/bspds/deploy/quadlets/*.container /etc/containers/systemd/ 59 ``` 60 Note: Systemd doesn't support shell-style variable expansion in `Environment=` lines. The quadlet files expect DATABASE_URL to be set in the environment file. 61 ## 5. Create nginx Configuration 62 ```bash 63 - cp /opt/bspds/deploy/nginx/nginx-quadlet.conf /srv/bspds/config/nginx.conf 64 ``` 65 - ## 6. Build BSPDS Image 66 ```bash 67 cd /opt 68 - git clone https://tangled.org/lewis.moe/bspds-sandbox bspds 69 - cd bspds 70 - podman build -t bspds:latest . 71 ``` 72 ## 7. Create Podman Secrets 73 ```bash 74 - source /srv/bspds/config/bspds.env 75 - echo "$DB_PASSWORD" | podman secret create bspds-db-password - 76 - echo "$MINIO_ROOT_PASSWORD" | podman secret create bspds-minio-password - 77 ``` 78 ## 8. Start Services and Initialize 79 ```bash 80 systemctl daemon-reload 81 - systemctl start bspds-db bspds-minio bspds-valkey 82 sleep 10 83 ``` 84 85 Create the minio bucket: 86 ```bash 87 - podman run --rm --pod bspds \ 88 -e MINIO_ROOT_USER=minioadmin \ 89 -e MINIO_ROOT_PASSWORD=your-minio-password \ 90 docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ ··· 94 Run migrations: 95 ```bash 96 cargo install sqlx-cli --no-default-features --features postgres 97 - DATABASE_URL="postgres://bspds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/bspds/migrations 98 ``` 99 ## 9. Obtain Wildcard SSL Certificate 100 User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. ··· 102 Create temporary self-signed cert to start services: 103 ```bash 104 openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ 105 - -keyout /srv/bspds/certs/privkey.pem \ 106 - -out /srv/bspds/certs/fullchain.pem \ 107 -subj "/CN=pds.example.com" 108 - systemctl start bspds-app bspds-nginx 109 ``` 110 111 Get a wildcard certificate using DNS validation: 112 ```bash 113 podman run --rm -it \ 114 - -v /srv/bspds/certs:/etc/letsencrypt:Z \ 115 docker.io/certbot/certbot:v5.2.2 certonly \ 116 --manual --preferred-challenges dns \ 117 -d pds.example.com -d '*.pds.example.com' \ ··· 123 124 Link certificates and restart: 125 ```bash 126 - ln -sf /srv/bspds/certs/live/pds.example.com/fullchain.pem /srv/bspds/certs/fullchain.pem 127 - ln -sf /srv/bspds/certs/live/pds.example.com/privkey.pem /srv/bspds/certs/privkey.pem 128 - systemctl restart bspds-nginx 129 ``` 130 ## 10. Enable All Services 131 ```bash 132 - systemctl enable bspds-db bspds-minio bspds-valkey bspds-app bspds-nginx 133 ``` 134 ## 11. Configure Firewall 135 ```bash ··· 142 ## 12. Certificate Renewal 143 Add to root's crontab (`crontab -e`): 144 ``` 145 - 0 0 * * * podman run --rm -v /srv/bspds/certs:/etc/letsencrypt:Z -v /srv/bspds/acme:/var/www/acme:Z docker.io/certbot/certbot:v5.2.2 renew --quiet && systemctl reload bspds-nginx 146 ``` 147 --- 148 # Alpine 3.23+ with OpenRC ··· 161 ``` 162 ## 2. Create Directory Structure 163 ```sh 164 - mkdir -p /srv/bspds/{data,config} 165 - mkdir -p /srv/bspds/data/{postgres,minio,valkey,certs,acme} 166 ``` 167 ## 3. Clone Repository and Build 168 ```sh 169 cd /opt 170 - git clone https://tangled.org/lewis.moe/bspds-sandbox bspds 171 - cd bspds 172 - podman build -t bspds:latest . 173 ``` 174 ## 4. Create Environment File 175 ```sh 176 - cp /opt/bspds/.env.example /srv/bspds/config/bspds.env 177 - chmod 600 /srv/bspds/config/bspds.env 178 ``` 179 - Edit `/srv/bspds/config/bspds.env` and fill in your values. Generate secrets with: 180 ```sh 181 openssl rand -base64 48 182 ``` 183 ## 5. Set Up Compose and nginx 184 Copy the production compose and nginx configs: 185 ```sh 186 - cp /opt/bspds/docker-compose.prod.yml /srv/bspds/docker-compose.yml 187 - cp /opt/bspds/nginx.prod.conf /srv/bspds/config/nginx.conf 188 ``` 189 - Edit `/srv/bspds/docker-compose.yml` to adjust paths if needed: 190 - - Update volume mounts to use `/srv/bspds/data/` paths 191 - - Update nginx cert paths to match `/srv/bspds/data/certs/` 192 - Edit `/srv/bspds/config/nginx.conf` to update cert paths: 193 - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` 194 ## 6. Create OpenRC Service 195 ```sh 196 - cat > /etc/init.d/bspds << 'EOF' 197 #!/sbin/openrc-run 198 - name="bspds" 199 - description="BSPDS AT Protocol PDS (containerized)" 200 command="/usr/bin/podman-compose" 201 - command_args="-f /srv/bspds/docker-compose.yml up" 202 command_background=true 203 pidfile="/run/${RC_SVCNAME}.pid" 204 - directory="/srv/bspds" 205 depend() { 206 need net podman 207 after firewall 208 } 209 start_pre() { 210 set -a 211 - . /srv/bspds/config/bspds.env 212 set +a 213 } 214 stop() { 215 ebegin "Stopping ${name}" 216 - cd /srv/bspds 217 set -a 218 - . /srv/bspds/config/bspds.env 219 set +a 220 - podman-compose -f /srv/bspds/docker-compose.yml down 221 eend $? 222 } 223 EOF 224 - chmod +x /etc/init.d/bspds 225 ``` 226 ## 7. Initialize Services 227 Start services: 228 ```sh 229 - rc-service bspds start 230 sleep 15 231 ``` 232 233 Create the minio bucket: 234 ```sh 235 - source /srv/bspds/config/bspds.env 236 - podman run --rm --network bspds_default \ 237 -e MINIO_ROOT_USER="$MINIO_ROOT_USER" \ 238 -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ 239 docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ ··· 246 rustup-init -y 247 source ~/.cargo/env 248 cargo install sqlx-cli --no-default-features --features postgres 249 - DB_IP=$(podman inspect bspds-db-1 --format '{{.NetworkSettings.Networks.bspds_default.IPAddress}}') 250 - DATABASE_URL="postgres://bspds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/bspds/migrations 251 ``` 252 ## 8. Obtain Wildcard SSL Certificate 253 User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. ··· 255 Create temporary self-signed cert to start services: 256 ```sh 257 openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ 258 - -keyout /srv/bspds/data/certs/privkey.pem \ 259 - -out /srv/bspds/data/certs/fullchain.pem \ 260 -subj "/CN=pds.example.com" 261 - rc-service bspds restart 262 ``` 263 264 Get a wildcard certificate using DNS validation: 265 ```sh 266 podman run --rm -it \ 267 - -v /srv/bspds/data/certs:/etc/letsencrypt \ 268 docker.io/certbot/certbot:v5.2.2 certonly \ 269 --manual --preferred-challenges dns \ 270 -d pds.example.com -d '*.pds.example.com' \ ··· 274 275 Link certificates and restart: 276 ```sh 277 - ln -sf /srv/bspds/data/certs/live/pds.example.com/fullchain.pem /srv/bspds/data/certs/fullchain.pem 278 - ln -sf /srv/bspds/data/certs/live/pds.example.com/privkey.pem /srv/bspds/data/certs/privkey.pem 279 - rc-service bspds restart 280 ``` 281 ## 9. Enable Service at Boot 282 ```sh 283 - rc-update add bspds 284 ``` 285 ## 10. Configure Firewall 286 ```sh ··· 305 ## 11. Certificate Renewal 306 Add to root's crontab (`crontab -e`): 307 ``` 308 - 0 0 * * * podman run --rm -v /srv/bspds/data/certs:/etc/letsencrypt -v /srv/bspds/data/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service bspds restart 309 ``` 310 --- 311 # Verification and Maintenance ··· 317 ## View Logs 318 **Debian:** 319 ```bash 320 - journalctl -u bspds-app -f 321 - podman logs -f bspds-app 322 ``` 323 **Alpine:** 324 ```sh 325 - podman-compose -f /srv/bspds/docker-compose.yml logs -f 326 - podman logs -f bspds-bspds-1 327 ``` 328 - ## Update BSPDS 329 ```sh 330 - cd /opt/bspds 331 git pull 332 - podman build -t bspds:latest . 333 ``` 334 335 Debian: 336 ```bash 337 - systemctl restart bspds-app 338 ``` 339 340 Alpine: 341 ```sh 342 - rc-service bspds restart 343 ``` 344 ## Backup Database 345 **Debian:** 346 ```bash 347 - podman exec bspds-db pg_dump -U bspds pds > /var/backups/pds-$(date +%Y%m%d).sql 348 ``` 349 **Alpine:** 350 ```sh 351 - podman exec bspds-db-1 pg_dump -U bspds pds > /var/backups/pds-$(date +%Y%m%d).sql 352 ```
··· 1 + # Tranquil PDS Containerized Production Deployment 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 + This guide covers deploying Tranquil PDS using containers with podman. 4 - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) 5 - **Alpine 3.23+**: Uses OpenRC service script with podman-compose 6 ## Prerequisites ··· 39 ## 2. Create Directory Structure 40 ```bash 41 mkdir -p /etc/containers/systemd 42 + mkdir -p /srv/tranquil-pds/{postgres,minio,valkey,certs,acme,config} 43 ``` 44 ## 3. Create Environment File 45 ```bash 46 + cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 47 + chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 48 ``` 49 + Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 50 ```bash 51 openssl rand -base64 48 52 ``` ··· 54 ## 4. Install Quadlet Definitions 55 Copy the quadlet files from the repository: 56 ```bash 57 + cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/ 58 + cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/ 59 ``` 60 Note: Systemd doesn't support shell-style variable expansion in `Environment=` lines. The quadlet files expect DATABASE_URL to be set in the environment file. 61 ## 5. Create nginx Configuration 62 ```bash 63 + cp /opt/tranquil-pds/deploy/nginx/nginx-quadlet.conf /srv/tranquil-pds/config/nginx.conf 64 ``` 65 + ## 6. Build Tranquil PDS Image 66 ```bash 67 cd /opt 68 + git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 69 + cd tranquil-pds 70 + podman build -t tranquil-pds:latest . 71 ``` 72 ## 7. Create Podman Secrets 73 ```bash 74 + source /srv/tranquil-pds/config/tranquil-pds.env 75 + echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - 76 + echo "$MINIO_ROOT_PASSWORD" | podman secret create tranquil-pds-minio-password - 77 ``` 78 ## 8. Start Services and Initialize 79 ```bash 80 systemctl daemon-reload 81 + systemctl start tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey 82 sleep 10 83 ``` 84 85 Create the minio bucket: 86 ```bash 87 + podman run --rm --pod tranquil-pds \ 88 -e MINIO_ROOT_USER=minioadmin \ 89 -e MINIO_ROOT_PASSWORD=your-minio-password \ 90 docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ ··· 94 Run migrations: 95 ```bash 96 cargo install sqlx-cli --no-default-features --features postgres 97 + DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 98 ``` 99 ## 9. Obtain Wildcard SSL Certificate 100 User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. ··· 102 Create temporary self-signed cert to start services: 103 ```bash 104 openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ 105 + -keyout /srv/tranquil-pds/certs/privkey.pem \ 106 + -out /srv/tranquil-pds/certs/fullchain.pem \ 107 -subj "/CN=pds.example.com" 108 + systemctl start tranquil-pds-app tranquil-pds-nginx 109 ``` 110 111 Get a wildcard certificate using DNS validation: 112 ```bash 113 podman run --rm -it \ 114 + -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \ 115 docker.io/certbot/certbot:v5.2.2 certonly \ 116 --manual --preferred-challenges dns \ 117 -d pds.example.com -d '*.pds.example.com' \ ··· 123 124 Link certificates and restart: 125 ```bash 126 + ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem 127 + ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem 128 + systemctl restart tranquil-pds-nginx 129 ``` 130 ## 10. Enable All Services 131 ```bash 132 + systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-nginx 133 ``` 134 ## 11. Configure Firewall 135 ```bash ··· 142 ## 12. Certificate Renewal 143 Add to root's crontab (`crontab -e`): 144 ``` 145 + 0 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z -v /srv/tranquil-pds/acme:/var/www/acme:Z docker.io/certbot/certbot:v5.2.2 renew --quiet && systemctl reload tranquil-pds-nginx 146 ``` 147 --- 148 # Alpine 3.23+ with OpenRC ··· 161 ``` 162 ## 2. Create Directory Structure 163 ```sh 164 + mkdir -p /srv/tranquil-pds/{data,config} 165 + mkdir -p /srv/tranquil-pds/data/{postgres,minio,valkey,certs,acme} 166 ``` 167 ## 3. Clone Repository and Build 168 ```sh 169 cd /opt 170 + git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 171 + cd tranquil-pds 172 + podman build -t tranquil-pds:latest . 173 ``` 174 ## 4. Create Environment File 175 ```sh 176 + cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 177 + chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 178 ``` 179 + Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 180 ```sh 181 openssl rand -base64 48 182 ``` 183 ## 5. Set Up Compose and nginx 184 Copy the production compose and nginx configs: 185 ```sh 186 + cp /opt/tranquil-pds/docker-compose.prod.yml /srv/tranquil-pds/docker-compose.yml 187 + cp /opt/tranquil-pds/nginx.prod.conf /srv/tranquil-pds/config/nginx.conf 188 ``` 189 + Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed: 190 + - Update volume mounts to use `/srv/tranquil-pds/data/` paths 191 + - Update nginx cert paths to match `/srv/tranquil-pds/data/certs/` 192 + Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths: 193 - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` 194 ## 6. Create OpenRC Service 195 ```sh 196 + cat > /etc/init.d/tranquil-pds << 'EOF' 197 #!/sbin/openrc-run 198 + name="tranquil-pds" 199 + description="Tranquil PDS AT Protocol PDS (containerized)" 200 command="/usr/bin/podman-compose" 201 + command_args="-f /srv/tranquil-pds/docker-compose.yml up" 202 command_background=true 203 pidfile="/run/${RC_SVCNAME}.pid" 204 + directory="/srv/tranquil-pds" 205 depend() { 206 need net podman 207 after firewall 208 } 209 start_pre() { 210 set -a 211 + . /srv/tranquil-pds/config/tranquil-pds.env 212 set +a 213 } 214 stop() { 215 ebegin "Stopping ${name}" 216 + cd /srv/tranquil-pds 217 set -a 218 + . /srv/tranquil-pds/config/tranquil-pds.env 219 set +a 220 + podman-compose -f /srv/tranquil-pds/docker-compose.yml down 221 eend $? 222 } 223 EOF 224 + chmod +x /etc/init.d/tranquil-pds 225 ``` 226 ## 7. Initialize Services 227 Start services: 228 ```sh 229 + rc-service tranquil-pds start 230 sleep 15 231 ``` 232 233 Create the minio bucket: 234 ```sh 235 + source /srv/tranquil-pds/config/tranquil-pds.env 236 + podman run --rm --network tranquil-pds_default \ 237 -e MINIO_ROOT_USER="$MINIO_ROOT_USER" \ 238 -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ 239 docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ ··· 246 rustup-init -y 247 source ~/.cargo/env 248 cargo install sqlx-cli --no-default-features --features postgres 249 + DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}') 250 + DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 251 ``` 252 ## 8. Obtain Wildcard SSL Certificate 253 User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. ··· 255 Create temporary self-signed cert to start services: 256 ```sh 257 openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ 258 + -keyout /srv/tranquil-pds/data/certs/privkey.pem \ 259 + -out /srv/tranquil-pds/data/certs/fullchain.pem \ 260 -subj "/CN=pds.example.com" 261 + rc-service tranquil-pds restart 262 ``` 263 264 Get a wildcard certificate using DNS validation: 265 ```sh 266 podman run --rm -it \ 267 + -v /srv/tranquil-pds/data/certs:/etc/letsencrypt \ 268 docker.io/certbot/certbot:v5.2.2 certonly \ 269 --manual --preferred-challenges dns \ 270 -d pds.example.com -d '*.pds.example.com' \ ··· 274 275 Link certificates and restart: 276 ```sh 277 + ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/data/certs/fullchain.pem 278 + ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem 279 + rc-service tranquil-pds restart 280 ``` 281 ## 9. Enable Service at Boot 282 ```sh 283 + rc-update add tranquil-pds 284 ``` 285 ## 10. Configure Firewall 286 ```sh ··· 305 ## 11. Certificate Renewal 306 Add to root's crontab (`crontab -e`): 307 ``` 308 + 0 0 * * * podman run --rm -v /srv/tranquil-pds/data/certs:/etc/letsencrypt -v /srv/tranquil-pds/data/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service tranquil-pds restart 309 ``` 310 --- 311 # Verification and Maintenance ··· 317 ## View Logs 318 **Debian:** 319 ```bash 320 + journalctl -u tranquil-pds-app -f 321 + podman logs -f tranquil-pds-app 322 ``` 323 **Alpine:** 324 ```sh 325 + podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f 326 + podman logs -f tranquil-pds-tranquil-pds-1 327 ``` 328 + ## Update Tranquil PDS 329 ```sh 330 + cd /opt/tranquil-pds 331 git pull 332 + podman build -t tranquil-pds:latest . 333 ``` 334 335 Debian: 336 ```bash 337 + systemctl restart tranquil-pds-app 338 ``` 339 340 Alpine: 341 ```sh 342 + rc-service tranquil-pds restart 343 ``` 344 ## Backup Database 345 **Debian:** 346 ```bash 347 + podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql 348 ``` 349 **Alpine:** 350 ```sh 351 + podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql 352 ```
+40 -43
docs/install-debian.md
··· 1 - # BSPDS Production Installation on Debian 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 4 - This guide covers installing BSPDS on Debian 13 "Trixie" (current stable as of December 2025). 5 6 ## Prerequisites 7 - A VPS with at least 2GB RAM and 20GB disk ··· 19 source ~/.cargo/env 20 rustup default stable 21 ``` 22 - This installs the latest stable Rust (1.92+ as of December 2025). 23 ## 3. Install postgres 24 - Debian 13 includes PostgreSQL 17: 25 ```bash 26 apt install -y postgresql postgresql-contrib 27 systemctl enable postgresql 28 systemctl start postgresql 29 - sudo -u postgres psql -c "CREATE USER bspds WITH PASSWORD 'your-secure-password';" 30 - sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 31 - sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;" 32 ``` 33 ## 4. Install minio 34 ```bash ··· 71 mc mb local/pds-blobs 72 ``` 73 ## 5. Install valkey 74 - Debian 13 includes Valkey 8: 75 ```bash 76 apt install -y valkey 77 systemctl enable valkey-server ··· 83 export PATH="$HOME/.deno/bin:$PATH" 84 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 85 ``` 86 - ## 7. Clone and Build BSPDS 87 ```bash 88 cd /opt 89 - git clone https://tangled.org/lewis.moe/bspds-sandbox bspds 90 - cd bspds 91 cd frontend 92 deno task build 93 cd .. ··· 96 ## 8. Install sqlx-cli and Run Migrations 97 ```bash 98 cargo install sqlx-cli --no-default-features --features postgres 99 - export DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" 100 sqlx migrate run 101 ``` 102 - ## 9. Configure BSPDS 103 ```bash 104 - mkdir -p /etc/bspds 105 - cp /opt/bspds/.env.example /etc/bspds/bspds.env 106 - chmod 600 /etc/bspds/bspds.env 107 ``` 108 - Edit `/etc/bspds/bspds.env` and fill in your values. Generate secrets with: 109 ```bash 110 openssl rand -base64 48 111 ``` 112 ## 10. Create Systemd Service 113 ```bash 114 - useradd -r -s /sbin/nologin bspds 115 - cp /opt/bspds/target/release/bspds /usr/local/bin/ 116 - mkdir -p /var/lib/bspds 117 - cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend 118 - chown -R bspds:bspds /var/lib/bspds 119 - cat > /etc/systemd/system/bspds.service << 'EOF' 120 [Unit] 121 - Description=BSPDS - AT Protocol PDS 122 After=network.target postgresql.service minio.service 123 [Service] 124 Type=simple 125 - User=bspds 126 - Group=bspds 127 - EnvironmentFile=/etc/bspds/bspds.env 128 - Environment=FRONTEND_DIR=/var/lib/bspds/frontend 129 - ExecStart=/usr/local/bin/bspds 130 Restart=always 131 RestartSec=5 132 [Install] 133 WantedBy=multi-user.target 134 EOF 135 systemctl daemon-reload 136 - systemctl enable bspds 137 - systemctl start bspds 138 ``` 139 ## 11. Install and Configure nginx 140 - Debian 13 includes nginx 1.26: 141 ```bash 142 apt install -y nginx certbot python3-certbot-nginx 143 - cat > /etc/nginx/sites-available/bspds << 'EOF' 144 server { 145 listen 80; 146 listen [::]:80; ··· 158 } 159 } 160 EOF 161 - ln -s /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/ 162 rm -f /etc/nginx/sites-enabled/default 163 nginx -t 164 systemctl reload nginx ··· 192 ``` 193 ## 14. Verify Installation 194 ```bash 195 - systemctl status bspds 196 curl -s https://pds.example.com/xrpc/_health | jq 197 curl -s https://pds.example.com/.well-known/atproto-did 198 ``` 199 ## Maintenance 200 View logs: 201 ```bash 202 - journalctl -u bspds -f 203 ``` 204 - Update BSPDS: 205 ```bash 206 - cd /opt/bspds 207 git pull 208 cd frontend && deno task build && cd .. 209 cargo build --release 210 - systemctl stop bspds 211 - cp target/release/bspds /usr/local/bin/ 212 - cp -r frontend/dist /var/lib/bspds/frontend 213 - DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" sqlx migrate run 214 - systemctl start bspds 215 ``` 216 Backup database: 217 ```bash
··· 1 + # Tranquil PDS Production Installation on Debian 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 4 + This guide covers installing Tranquil PDS on Debian 13 "Trixie". 5 6 ## Prerequisites 7 - A VPS with at least 2GB RAM and 20GB disk ··· 19 source ~/.cargo/env 20 rustup default stable 21 ``` 22 + This installs the latest stable Rust. 23 ## 3. Install postgres 24 ```bash 25 apt install -y postgresql postgresql-contrib 26 systemctl enable postgresql 27 systemctl start postgresql 28 + sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD 'your-secure-password';" 29 + sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 30 + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 31 ``` 32 ## 4. Install minio 33 ```bash ··· 70 mc mb local/pds-blobs 71 ``` 72 ## 5. Install valkey 73 ```bash 74 apt install -y valkey 75 systemctl enable valkey-server ··· 81 export PATH="$HOME/.deno/bin:$PATH" 82 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 83 ``` 84 + ## 7. Clone and Build Tranquil PDS 85 ```bash 86 cd /opt 87 + git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 88 + cd tranquil-pds 89 cd frontend 90 deno task build 91 cd .. ··· 94 ## 8. Install sqlx-cli and Run Migrations 95 ```bash 96 cargo install sqlx-cli --no-default-features --features postgres 97 + export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 98 sqlx migrate run 99 ``` 100 + ## 9. Configure Tranquil PDS 101 ```bash 102 + mkdir -p /etc/tranquil-pds 103 + cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env 104 + chmod 600 /etc/tranquil-pds/tranquil-pds.env 105 ``` 106 + Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with: 107 ```bash 108 openssl rand -base64 48 109 ``` 110 ## 10. Create Systemd Service 111 ```bash 112 + useradd -r -s /sbin/nologin tranquil-pds 113 + cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 114 + mkdir -p /var/lib/tranquil-pds 115 + cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend 116 + chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds 117 + cat > /etc/systemd/system/tranquil-pds.service << 'EOF' 118 [Unit] 119 + Description=Tranquil PDS - AT Protocol PDS 120 After=network.target postgresql.service minio.service 121 [Service] 122 Type=simple 123 + User=tranquil-pds 124 + Group=tranquil-pds 125 + EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 126 + Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 127 + ExecStart=/usr/local/bin/tranquil-pds 128 Restart=always 129 RestartSec=5 130 [Install] 131 WantedBy=multi-user.target 132 EOF 133 systemctl daemon-reload 134 + systemctl enable tranquil-pds 135 + systemctl start tranquil-pds 136 ``` 137 ## 11. Install and Configure nginx 138 ```bash 139 apt install -y nginx certbot python3-certbot-nginx 140 + cat > /etc/nginx/sites-available/tranquil-pds << 'EOF' 141 server { 142 listen 80; 143 listen [::]:80; ··· 155 } 156 } 157 EOF 158 + ln -s /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 159 rm -f /etc/nginx/sites-enabled/default 160 nginx -t 161 systemctl reload nginx ··· 189 ``` 190 ## 14. Verify Installation 191 ```bash 192 + systemctl status tranquil-pds 193 curl -s https://pds.example.com/xrpc/_health | jq 194 curl -s https://pds.example.com/.well-known/atproto-did 195 ``` 196 ## Maintenance 197 View logs: 198 ```bash 199 + journalctl -u tranquil-pds -f 200 ``` 201 + Update Tranquil PDS: 202 ```bash 203 + cd /opt/tranquil-pds 204 git pull 205 cd frontend && deno task build && cd .. 206 cargo build --release 207 + systemctl stop tranquil-pds 208 + cp target/release/tranquil-pds /usr/local/bin/ 209 + cp -r frontend/dist /var/lib/tranquil-pds/frontend 210 + DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 211 + systemctl start tranquil-pds 212 ``` 213 Backup database: 214 ```bash
+1 -1
docs/install-kubernetes.md
··· 1 - # BSPDS on Kubernetes 2 3 If you're reaching for kubernetes for this app, you're experienced enough to know how to spin up: 4
··· 1 + # Tranquil PDS on Kubernetes 2 3 If you're reaching for kubernetes for this app, you're experienced enough to know how to spin up: 4
+36 -37
docs/install-openbsd.md
··· 1 - # BSPDS Production Installation on OpenBSD 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 - This guide covers installing BSPDS on OpenBSD 7.8 (current release as of December 2025). 4 ## Prerequisites 5 - A VPS with at least 2GB RAM and 20GB disk 6 - A domain name pointing to your server's IP ··· 16 ```sh 17 pkg_add rust 18 ``` 19 - OpenBSD 7.8 ships Rust 1.82+. For the latest stable (1.92+), use rustup: 20 ```sh 21 pkg_add rustup 22 rustup-init -y ··· 24 rustup default stable 25 ``` 26 ## 3. Install postgres 27 - OpenBSD 7.8 includes PostgreSQL 17 (PostgreSQL 18 may not yet be in ports): 28 ```sh 29 pkg_add postgresql-server postgresql-client 30 mkdir -p /var/postgresql/data ··· 32 su - _postgresql -c "initdb -D /var/postgresql/data -U postgres -A scram-sha-256" 33 rcctl enable postgresql 34 rcctl start postgresql 35 - psql -U postgres -c "CREATE USER bspds WITH PASSWORD 'your-secure-password';" 36 - psql -U postgres -c "CREATE DATABASE pds OWNER bspds;" 37 - psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;" 38 ``` 39 ## 4. Install minio 40 OpenBSD doesn't have a minio package. Options: ··· 93 export PATH="$HOME/.deno/bin:$PATH" 94 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile 95 ``` 96 - ## 7. Clone and Build BSPDS 97 ```sh 98 mkdir -p /opt && cd /opt 99 - git clone https://tangled.org/lewis.moe/bspds-sandbox bspds 100 - cd bspds 101 cd frontend 102 deno task build 103 cd .. ··· 106 ## 8. Install sqlx-cli and Run Migrations 107 ```sh 108 cargo install sqlx-cli --no-default-features --features postgres 109 - export DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" 110 sqlx migrate run 111 ``` 112 - ## 9. Configure BSPDS 113 ```sh 114 - mkdir -p /etc/bspds 115 - cp /opt/bspds/.env.example /etc/bspds/bspds.conf 116 - chmod 600 /etc/bspds/bspds.conf 117 ``` 118 - Edit `/etc/bspds/bspds.conf` and fill in your values. Generate secrets with: 119 ```sh 120 openssl rand -base64 48 121 ``` 122 ## 10. Create rc.d Service 123 ```sh 124 - useradd -d /var/empty -s /sbin/nologin _bspds 125 - cp /opt/bspds/target/release/bspds /usr/local/bin/ 126 - mkdir -p /var/bspds 127 - cp -r /opt/bspds/frontend/dist /var/bspds/frontend 128 - chown -R _bspds:_bspds /var/bspds 129 - cat > /etc/rc.d/bspds << 'EOF' 130 #!/bin/ksh 131 - daemon="/usr/local/bin/bspds" 132 - daemon_user="_bspds" 133 daemon_logger="daemon.info" 134 . /etc/rc.d/rc.subr 135 rc_pre() { 136 - export FRONTEND_DIR=/var/bspds/frontend 137 while IFS='=' read -r key value; do 138 case "$key" in 139 \#*|"") continue ;; 140 esac 141 export "$key=$value" 142 - done < /etc/bspds/bspds.conf 143 } 144 rc_cmd $1 145 EOF 146 - chmod +x /etc/rc.d/bspds 147 - rcctl enable bspds 148 - rcctl start bspds 149 ``` 150 ## 11. Install and Configure nginx 151 ```sh ··· 227 ``` 228 ## 14. Verify Installation 229 ```sh 230 - rcctl check bspds 231 ftp -o - https://pds.example.com/xrpc/_health 232 ftp -o - https://pds.example.com/.well-known/atproto-did 233 ``` ··· 236 ```sh 237 tail -f /var/log/daemon 238 ``` 239 - Update BSPDS: 240 ```sh 241 - cd /opt/bspds 242 git pull 243 cd frontend && deno task build && cd .. 244 cargo build --release 245 - rcctl stop bspds 246 - cp target/release/bspds /usr/local/bin/ 247 - cp -r frontend/dist /var/bspds/frontend 248 - DATABASE_URL="postgres://bspds:your-secure-password@localhost:5432/pds" sqlx migrate run 249 - rcctl start bspds 250 ``` 251 Backup database: 252 ```sh
··· 1 + # Tranquil PDS Production Installation on OpenBSD 2 > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 + This guide covers installing Tranquil PDS on OpenBSD 7.8. 4 ## Prerequisites 5 - A VPS with at least 2GB RAM and 20GB disk 6 - A domain name pointing to your server's IP ··· 16 ```sh 17 pkg_add rust 18 ``` 19 + OpenBSD ships Rust in ports. For the latest stable, use rustup: 20 ```sh 21 pkg_add rustup 22 rustup-init -y ··· 24 rustup default stable 25 ``` 26 ## 3. Install postgres 27 ```sh 28 pkg_add postgresql-server postgresql-client 29 mkdir -p /var/postgresql/data ··· 31 su - _postgresql -c "initdb -D /var/postgresql/data -U postgres -A scram-sha-256" 32 rcctl enable postgresql 33 rcctl start postgresql 34 + psql -U postgres -c "CREATE USER tranquil_pds WITH PASSWORD 'your-secure-password';" 35 + psql -U postgres -c "CREATE DATABASE pds OWNER tranquil_pds;" 36 + psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 37 ``` 38 ## 4. Install minio 39 OpenBSD doesn't have a minio package. Options: ··· 92 export PATH="$HOME/.deno/bin:$PATH" 93 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile 94 ``` 95 + ## 7. Clone and Build Tranquil PDS 96 ```sh 97 mkdir -p /opt && cd /opt 98 + git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 99 + cd tranquil-pds 100 cd frontend 101 deno task build 102 cd .. ··· 105 ## 8. Install sqlx-cli and Run Migrations 106 ```sh 107 cargo install sqlx-cli --no-default-features --features postgres 108 + export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 109 sqlx migrate run 110 ``` 111 + ## 9. Configure Tranquil PDS 112 ```sh 113 + mkdir -p /etc/tranquil-pds 114 + cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.conf 115 + chmod 600 /etc/tranquil-pds/tranquil-pds.conf 116 ``` 117 + Edit `/etc/tranquil-pds/tranquil-pds.conf` and fill in your values. Generate secrets with: 118 ```sh 119 openssl rand -base64 48 120 ``` 121 ## 10. Create rc.d Service 122 ```sh 123 + useradd -d /var/empty -s /sbin/nologin _tranquil_pds 124 + cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 125 + mkdir -p /var/tranquil-pds 126 + cp -r /opt/tranquil-pds/frontend/dist /var/tranquil-pds/frontend 127 + chown -R _tranquil_pds:_tranquil_pds /var/tranquil-pds 128 + cat > /etc/rc.d/tranquil_pds << 'EOF' 129 #!/bin/ksh 130 + daemon="/usr/local/bin/tranquil-pds" 131 + daemon_user="_tranquil_pds" 132 daemon_logger="daemon.info" 133 . /etc/rc.d/rc.subr 134 rc_pre() { 135 + export FRONTEND_DIR=/var/tranquil-pds/frontend 136 while IFS='=' read -r key value; do 137 case "$key" in 138 \#*|"") continue ;; 139 esac 140 export "$key=$value" 141 + done < /etc/tranquil-pds/tranquil-pds.conf 142 } 143 rc_cmd $1 144 EOF 145 + chmod +x /etc/rc.d/tranquil_pds 146 + rcctl enable tranquil_pds 147 + rcctl start tranquil_pds 148 ``` 149 ## 11. Install and Configure nginx 150 ```sh ··· 226 ``` 227 ## 14. Verify Installation 228 ```sh 229 + rcctl check tranquil_pds 230 ftp -o - https://pds.example.com/xrpc/_health 231 ftp -o - https://pds.example.com/.well-known/atproto-did 232 ``` ··· 235 ```sh 236 tail -f /var/log/daemon 237 ``` 238 + Update Tranquil PDS: 239 ```sh 240 + cd /opt/tranquil-pds 241 git pull 242 cd frontend && deno task build && cd .. 243 cargo build --release 244 + rcctl stop tranquil_pds 245 + cp target/release/tranquil-pds /usr/local/bin/ 246 + cp -r frontend/dist /var/tranquil-pds/frontend 247 + DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 248 + rcctl start tranquil_pds 249 ``` 250 Backup database: 251 ```sh
+1 -1
frontend/index.html
··· 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>BSPDS</title> 7 <style> 8 html { background: #fafafa; } 9 @media (prefers-color-scheme: dark) { html { background: #1a1a1a; } }
··· 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Tranquil PDS</title> 7 <style> 8 html { background: #fafafa; } 9 @media (prefers-color-scheme: dark) { html { background: #1a1a1a; } }
+1 -1
frontend/package.json
··· 1 { 2 - "name": "bspds-frontend", 3 "private": true, 4 "version": "0.0.0", 5 "type": "module",
··· 1 { 2 + "name": "tranquil-pds-frontend", 3 "private": true, 4 "version": "0.0.0", 5 "type": "module",
+8 -8
frontend/src/lib/api.ts
··· 255 signalNumber: string | null 256 signalVerified: boolean 257 }> { 258 - return xrpc('com.bspds.account.getNotificationPrefs', { token }) 259 }, 260 261 async updateNotificationPrefs(token: string, prefs: { ··· 264 telegramUsername?: string 265 signalNumber?: string 266 }): Promise<{ success: boolean }> { 267 - return xrpc('com.bspds.account.updateNotificationPrefs', { 268 method: 'POST', 269 token, 270 body: prefs, ··· 272 }, 273 274 async confirmChannelVerification(token: string, channel: string, code: string): Promise<{ success: boolean }> { 275 - return xrpc('com.bspds.account.confirmChannelVerification', { 276 method: 'POST', 277 token, 278 body: { channel, code }, ··· 289 body: string 290 }> 291 }> { 292 - return xrpc('com.bspds.account.getNotificationHistory', { token }) 293 }, 294 295 async getServerStats(token: string): Promise<{ ··· 298 recordCount: number 299 blobStorageBytes: number 300 }> { 301 - return xrpc('com.bspds.admin.getServerStats', { token }) 302 }, 303 304 async changePassword(token: string, currentPassword: string, newPassword: string): Promise<void> { 305 - await xrpc('com.bspds.account.changePassword', { 306 method: 'POST', 307 token, 308 body: { currentPassword, newPassword }, ··· 317 isCurrent: boolean 318 }> 319 }> { 320 - return xrpc('com.bspds.account.listSessions', { token }) 321 }, 322 323 async revokeSession(token: string, sessionId: string): Promise<void> { 324 - await xrpc('com.bspds.account.revokeSession', { 325 method: 'POST', 326 token, 327 body: { sessionId },
··· 255 signalNumber: string | null 256 signalVerified: boolean 257 }> { 258 + return xrpc('com.tranquil.account.getNotificationPrefs', { token }) 259 }, 260 261 async updateNotificationPrefs(token: string, prefs: { ··· 264 telegramUsername?: string 265 signalNumber?: string 266 }): Promise<{ success: boolean }> { 267 + return xrpc('com.tranquil.account.updateNotificationPrefs', { 268 method: 'POST', 269 token, 270 body: prefs, ··· 272 }, 273 274 async confirmChannelVerification(token: string, channel: string, code: string): Promise<{ success: boolean }> { 275 + return xrpc('com.tranquil.account.confirmChannelVerification', { 276 method: 'POST', 277 token, 278 body: { channel, code }, ··· 289 body: string 290 }> 291 }> { 292 + return xrpc('com.tranquil.account.getNotificationHistory', { token }) 293 }, 294 295 async getServerStats(token: string): Promise<{ ··· 298 recordCount: number 299 blobStorageBytes: number 300 }> { 301 + return xrpc('com.tranquil.admin.getServerStats', { token }) 302 }, 303 304 async changePassword(token: string, currentPassword: string, newPassword: string): Promise<void> { 305 + await xrpc('com.tranquil.account.changePassword', { 306 method: 'POST', 307 token, 308 body: { currentPassword, newPassword }, ··· 317 isCurrent: boolean 318 }> 319 }> { 320 + return xrpc('com.tranquil.account.listSessions', { token }) 321 }, 322 323 async revokeSession(token: string, sessionId: string): Promise<void> { 324 + await xrpc('com.tranquil.account.revokeSession', { 325 method: 'POST', 326 token, 327 body: { sessionId },
+2 -2
frontend/src/lib/auth.svelte.ts
··· 1 import { api, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api' 2 import { startOAuthLogin, handleOAuthCallback, checkForOAuthCallback, clearOAuthCallbackParams, refreshOAuthToken } from './oauth' 3 4 - const STORAGE_KEY = 'bspds_session' 5 - const ACCOUNTS_KEY = 'bspds_accounts' 6 7 export interface SavedAccount { 8 did: string
··· 1 import { api, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api' 2 import { startOAuthLogin, handleOAuthCallback, checkForOAuthCallback, clearOAuthCallbackParams, refreshOAuthToken } from './oauth' 3 4 + const STORAGE_KEY = 'tranquil_pds_session' 5 + const ACCOUNTS_KEY = 'tranquil_pds_accounts' 6 7 export interface SavedAccount { 8 did: string
+2 -2
frontend/src/lib/oauth.ts
··· 1 - const OAUTH_STATE_KEY = 'bspds_oauth_state' 2 - const OAUTH_VERIFIER_KEY = 'bspds_oauth_verifier' 3 4 interface OAuthState { 5 state: string
··· 1 + const OAUTH_STATE_KEY = 'tranquil_pds_oauth_state' 2 + const OAUTH_VERIFIER_KEY = 'tranquil_pds_oauth_verifier' 3 4 interface OAuthState { 5 state: string
+1 -1
frontend/src/routes/Register.svelte
··· 3 import { navigate } from '../lib/router.svelte' 4 import { api, ApiError, type VerificationChannel } from '../lib/api' 5 6 - const STORAGE_KEY = 'bspds_pending_verification' 7 8 let handle = $state('') 9 let email = $state('')
··· 3 import { navigate } from '../lib/router.svelte' 4 import { api, ApiError, type VerificationChannel } from '../lib/api' 5 6 + const STORAGE_KEY = 'tranquil_pds_pending_verification' 7 8 let handle = $state('') 9 let email = $state('')
+1 -1
frontend/src/routes/Verify.svelte
··· 2 import { confirmSignup, resendVerification, getAuthState } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 5 - const STORAGE_KEY = 'bspds_pending_verification' 6 7 interface PendingVerification { 8 did: string
··· 2 import { confirmSignup, resendVerification, getAuthState } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 5 + const STORAGE_KEY = 'tranquil_pds_pending_verification' 6 7 interface PendingVerification { 8 did: string
+3 -3
frontend/src/tests/Dashboard.test.ts
··· 10 setupAuthenticatedUser, 11 setupUnauthenticatedUser, 12 } from './mocks' 13 - const STORAGE_KEY = 'bspds_session' 14 describe('Dashboard', () => { 15 beforeEach(() => { 16 clearMocks() ··· 38 await waitFor(() => { 39 expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument() 40 expect(screen.getByRole('heading', { name: /account overview/i })).toBeInTheDocument() 41 - expect(screen.getByText(/@testuser\.test\.bspds\.dev/)).toBeInTheDocument() 42 - expect(screen.getByText(/did:web:test\.bspds\.dev:u:testuser/)).toBeInTheDocument() 43 expect(screen.getByText('test@example.com')).toBeInTheDocument() 44 expect(screen.getByText('Verified')).toBeInTheDocument() 45 expect(screen.getByText('Verified')).toHaveClass('badge', 'success')
··· 10 setupAuthenticatedUser, 11 setupUnauthenticatedUser, 12 } from './mocks' 13 + const STORAGE_KEY = 'tranquil_pds_session' 14 describe('Dashboard', () => { 15 beforeEach(() => { 16 clearMocks() ··· 38 await waitFor(() => { 39 expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument() 40 expect(screen.getByRole('heading', { name: /account overview/i })).toBeInTheDocument() 41 + expect(screen.getByText(/@testuser\.test\.tranquil\.dev/)).toBeInTheDocument() 42 + expect(screen.getByText(/did:web:test\.tranquil\.dev:u:testuser/)).toBeInTheDocument() 43 expect(screen.getByText('test@example.com')).toBeInTheDocument() 44 expect(screen.getByText('Verified')).toBeInTheDocument() 45 expect(screen.getByText('Verified')).toHaveClass('badge', 'success')
+2 -2
frontend/src/tests/Login.test.ts
··· 95 json: async () => ({ 96 error: 'AccountNotVerified', 97 message: 'Account not verified', 98 - did: 'did:web:test.bspds.dev:u:testuser', 99 }), 100 })) 101 render(Login) ··· 116 json: async () => ({ 117 error: 'AccountNotVerified', 118 message: 'Account not verified', 119 - did: 'did:web:test.bspds.dev:u:testuser', 120 }), 121 })) 122 render(Login)
··· 95 json: async () => ({ 96 error: 'AccountNotVerified', 97 message: 'Account not verified', 98 + did: 'did:web:test.tranquil.dev:u:testuser', 99 }), 100 })) 101 render(Login) ··· 116 json: async () => ({ 117 error: 'AccountNotVerified', 118 message: 'Account not verified', 119 + did: 'did:web:test.tranquil.dev:u:testuser', 120 }), 121 })) 122 render(Login)
+27 -27
frontend/src/tests/Notifications.test.ts
··· 28 describe('page structure', () => { 29 beforeEach(() => { 30 setupAuthenticatedUser() 31 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 32 jsonResponse(mockData.notificationPrefs()) 33 ) 34 }) ··· 48 setupAuthenticatedUser() 49 }) 50 it('shows loading text while fetching preferences', async () => { 51 - mockEndpoint('com.bspds.account.getNotificationPrefs', async () => { 52 await new Promise(resolve => setTimeout(resolve, 100)) 53 return jsonResponse(mockData.notificationPrefs()) 54 }) ··· 61 setupAuthenticatedUser() 62 }) 63 it('displays all four channel options', async () => { 64 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 65 jsonResponse(mockData.notificationPrefs()) 66 ) 67 render(Notifications) ··· 73 }) 74 }) 75 it('email channel is always selectable', async () => { 76 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 77 jsonResponse(mockData.notificationPrefs()) 78 ) 79 render(Notifications) ··· 83 }) 84 }) 85 it('discord channel is disabled when not configured', async () => { 86 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 87 jsonResponse(mockData.notificationPrefs({ discordId: null })) 88 ) 89 render(Notifications) ··· 93 }) 94 }) 95 it('discord channel is enabled when configured', async () => { 96 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 97 jsonResponse(mockData.notificationPrefs({ discordId: '123456789' })) 98 ) 99 render(Notifications) ··· 103 }) 104 }) 105 it('shows hint for disabled channels', async () => { 106 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 107 jsonResponse(mockData.notificationPrefs()) 108 ) 109 render(Notifications) ··· 112 }) 113 }) 114 it('selects current preferred channel', async () => { 115 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 116 jsonResponse(mockData.notificationPrefs({ preferredChannel: 'email' })) 117 ) 118 render(Notifications) ··· 127 setupAuthenticatedUser() 128 }) 129 it('displays email as readonly with current value', async () => { 130 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 131 jsonResponse(mockData.notificationPrefs()) 132 ) 133 render(Notifications) ··· 138 }) 139 }) 140 it('displays all channel inputs with current values', async () => { 141 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 142 jsonResponse(mockData.notificationPrefs({ 143 discordId: '123456789', 144 telegramUsername: 'testuser', ··· 158 setupAuthenticatedUser() 159 }) 160 it('shows Primary badge for email', async () => { 161 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 162 jsonResponse(mockData.notificationPrefs()) 163 ) 164 render(Notifications) ··· 167 }) 168 }) 169 it('shows Verified badge for verified discord', async () => { 170 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 171 jsonResponse(mockData.notificationPrefs({ 172 discordId: '123456789', 173 discordVerified: true, ··· 180 }) 181 }) 182 it('shows Not verified badge for unverified discord', async () => { 183 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 184 jsonResponse(mockData.notificationPrefs({ 185 discordId: '123456789', 186 discordVerified: false, ··· 192 }) 193 }) 194 it('does not show badge when channel not configured', async () => { 195 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 196 jsonResponse(mockData.notificationPrefs()) 197 ) 198 render(Notifications) ··· 208 }) 209 it('calls updateNotificationPrefs with correct data', async () => { 210 let capturedBody: Record<string, unknown> | null = null 211 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 212 jsonResponse(mockData.notificationPrefs()) 213 ) 214 - mockEndpoint('com.bspds.account.updateNotificationPrefs', (_url, options) => { 215 capturedBody = JSON.parse((options?.body as string) || '{}') 216 return jsonResponse({ success: true }) 217 }) ··· 228 }) 229 }) 230 it('shows loading state while saving', async () => { 231 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 232 jsonResponse(mockData.notificationPrefs()) 233 ) 234 - mockEndpoint('com.bspds.account.updateNotificationPrefs', async () => { 235 await new Promise(resolve => setTimeout(resolve, 100)) 236 return jsonResponse({ success: true }) 237 }) ··· 244 expect(screen.getByRole('button', { name: /saving/i })).toBeDisabled() 245 }) 246 it('shows success message after saving', async () => { 247 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 248 jsonResponse(mockData.notificationPrefs()) 249 ) 250 - mockEndpoint('com.bspds.account.updateNotificationPrefs', () => 251 jsonResponse({ success: true }) 252 ) 253 render(Notifications) ··· 260 }) 261 }) 262 it('shows error when save fails', async () => { 263 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 264 jsonResponse(mockData.notificationPrefs()) 265 ) 266 - mockEndpoint('com.bspds.account.updateNotificationPrefs', () => 267 errorResponse('InvalidRequest', 'Invalid channel configuration', 400) 268 ) 269 render(Notifications) ··· 278 }) 279 it('reloads preferences after successful save', async () => { 280 let loadCount = 0 281 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => { 282 loadCount++ 283 return jsonResponse(mockData.notificationPrefs()) 284 }) 285 - mockEndpoint('com.bspds.account.updateNotificationPrefs', () => 286 jsonResponse({ success: true }) 287 ) 288 render(Notifications) ··· 301 setupAuthenticatedUser() 302 }) 303 it('enables discord channel after entering discord ID', async () => { 304 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 305 jsonResponse(mockData.notificationPrefs()) 306 ) 307 render(Notifications) ··· 314 }) 315 }) 316 it('allows selecting a configured channel', async () => { 317 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 318 jsonResponse(mockData.notificationPrefs({ 319 discordId: '123456789', 320 discordVerified: true, ··· 334 setupAuthenticatedUser() 335 }) 336 it('shows error when loading preferences fails', async () => { 337 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 338 errorResponse('InternalError', 'Database connection failed', 500) 339 ) 340 render(Notifications)
··· 28 describe('page structure', () => { 29 beforeEach(() => { 30 setupAuthenticatedUser() 31 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 32 jsonResponse(mockData.notificationPrefs()) 33 ) 34 }) ··· 48 setupAuthenticatedUser() 49 }) 50 it('shows loading text while fetching preferences', async () => { 51 + mockEndpoint('com.tranquil.account.getNotificationPrefs', async () => { 52 await new Promise(resolve => setTimeout(resolve, 100)) 53 return jsonResponse(mockData.notificationPrefs()) 54 }) ··· 61 setupAuthenticatedUser() 62 }) 63 it('displays all four channel options', async () => { 64 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 65 jsonResponse(mockData.notificationPrefs()) 66 ) 67 render(Notifications) ··· 73 }) 74 }) 75 it('email channel is always selectable', async () => { 76 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 77 jsonResponse(mockData.notificationPrefs()) 78 ) 79 render(Notifications) ··· 83 }) 84 }) 85 it('discord channel is disabled when not configured', async () => { 86 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 87 jsonResponse(mockData.notificationPrefs({ discordId: null })) 88 ) 89 render(Notifications) ··· 93 }) 94 }) 95 it('discord channel is enabled when configured', async () => { 96 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 97 jsonResponse(mockData.notificationPrefs({ discordId: '123456789' })) 98 ) 99 render(Notifications) ··· 103 }) 104 }) 105 it('shows hint for disabled channels', async () => { 106 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 107 jsonResponse(mockData.notificationPrefs()) 108 ) 109 render(Notifications) ··· 112 }) 113 }) 114 it('selects current preferred channel', async () => { 115 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 116 jsonResponse(mockData.notificationPrefs({ preferredChannel: 'email' })) 117 ) 118 render(Notifications) ··· 127 setupAuthenticatedUser() 128 }) 129 it('displays email as readonly with current value', async () => { 130 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 131 jsonResponse(mockData.notificationPrefs()) 132 ) 133 render(Notifications) ··· 138 }) 139 }) 140 it('displays all channel inputs with current values', async () => { 141 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 142 jsonResponse(mockData.notificationPrefs({ 143 discordId: '123456789', 144 telegramUsername: 'testuser', ··· 158 setupAuthenticatedUser() 159 }) 160 it('shows Primary badge for email', async () => { 161 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 162 jsonResponse(mockData.notificationPrefs()) 163 ) 164 render(Notifications) ··· 167 }) 168 }) 169 it('shows Verified badge for verified discord', async () => { 170 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 171 jsonResponse(mockData.notificationPrefs({ 172 discordId: '123456789', 173 discordVerified: true, ··· 180 }) 181 }) 182 it('shows Not verified badge for unverified discord', async () => { 183 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 184 jsonResponse(mockData.notificationPrefs({ 185 discordId: '123456789', 186 discordVerified: false, ··· 192 }) 193 }) 194 it('does not show badge when channel not configured', async () => { 195 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 196 jsonResponse(mockData.notificationPrefs()) 197 ) 198 render(Notifications) ··· 208 }) 209 it('calls updateNotificationPrefs with correct data', async () => { 210 let capturedBody: Record<string, unknown> | null = null 211 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 212 jsonResponse(mockData.notificationPrefs()) 213 ) 214 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', (_url, options) => { 215 capturedBody = JSON.parse((options?.body as string) || '{}') 216 return jsonResponse({ success: true }) 217 }) ··· 228 }) 229 }) 230 it('shows loading state while saving', async () => { 231 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 232 jsonResponse(mockData.notificationPrefs()) 233 ) 234 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', async () => { 235 await new Promise(resolve => setTimeout(resolve, 100)) 236 return jsonResponse({ success: true }) 237 }) ··· 244 expect(screen.getByRole('button', { name: /saving/i })).toBeDisabled() 245 }) 246 it('shows success message after saving', async () => { 247 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 248 jsonResponse(mockData.notificationPrefs()) 249 ) 250 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 251 jsonResponse({ success: true }) 252 ) 253 render(Notifications) ··· 260 }) 261 }) 262 it('shows error when save fails', async () => { 263 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 264 jsonResponse(mockData.notificationPrefs()) 265 ) 266 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 267 errorResponse('InvalidRequest', 'Invalid channel configuration', 400) 268 ) 269 render(Notifications) ··· 278 }) 279 it('reloads preferences after successful save', async () => { 280 let loadCount = 0 281 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => { 282 loadCount++ 283 return jsonResponse(mockData.notificationPrefs()) 284 }) 285 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 286 jsonResponse({ success: true }) 287 ) 288 render(Notifications) ··· 301 setupAuthenticatedUser() 302 }) 303 it('enables discord channel after entering discord ID', async () => { 304 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 305 jsonResponse(mockData.notificationPrefs()) 306 ) 307 render(Notifications) ··· 314 }) 315 }) 316 it('allows selecting a configured channel', async () => { 317 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 318 jsonResponse(mockData.notificationPrefs({ 319 discordId: '123456789', 320 discordVerified: true, ··· 334 setupAuthenticatedUser() 335 }) 336 it('shows error when loading preferences fails', async () => { 337 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 338 errorResponse('InternalError', 'Database connection failed', 500) 339 ) 340 render(Notifications)
+2 -2
frontend/src/tests/Settings.test.ts
··· 176 it('displays current handle', async () => { 177 render(Settings) 178 await waitFor(() => { 179 - expect(screen.getByText(/current: @testuser\.test\.bspds\.dev/i)).toBeInTheDocument() 180 }) 181 }) 182 it('calls updateHandle with new handle', async () => { ··· 314 await waitFor(() => { 315 expect(capturedBody?.token).toBe('DEL123') 316 expect(capturedBody?.password).toBe('mypassword') 317 - expect(capturedBody?.did).toBe('did:web:test.bspds.dev:u:testuser') 318 }) 319 }) 320 it('navigates to login after successful deletion', async () => {
··· 176 it('displays current handle', async () => { 177 render(Settings) 178 await waitFor(() => { 179 + expect(screen.getByText(/current: @testuser\.test\.tranquil\.dev/i)).toBeInTheDocument() 180 }) 181 }) 182 it('calls updateHandle with new handle', async () => { ··· 314 await waitFor(() => { 315 expect(capturedBody?.token).toBe('DEL123') 316 expect(capturedBody?.password).toBe('mypassword') 317 + expect(capturedBody?.did).toBe('did:web:test.tranquil.dev:u:testuser') 318 }) 319 }) 320 it('navigates to login after successful deletion', async () => {
+8 -8
frontend/src/tests/mocks.ts
··· 85 } 86 export const mockData = { 87 session: (overrides?: Partial<Session>): Session => ({ 88 - did: 'did:web:test.bspds.dev:u:testuser', 89 - handle: 'testuser.test.bspds.dev', 90 email: 'test@example.com', 91 emailConfirmed: true, 92 accessJwt: 'mock-access-jwt-token', ··· 102 code: 'test-invite-123', 103 available: 1, 104 disabled: false, 105 - forAccount: 'did:web:test.bspds.dev:u:testuser', 106 - createdBy: 'did:web:test.bspds.dev:u:testuser', 107 createdAt: new Date().toISOString(), 108 uses: [], 109 ...overrides, ··· 120 ...overrides, 121 }), 122 describeServer: () => ({ 123 - availableUserDomains: ['test.bspds.dev'], 124 inviteCodeRequired: false, 125 links: { 126 privacyPolicy: 'https://example.com/privacy', ··· 128 }, 129 }), 130 describeRepo: (did: string) => ({ 131 - handle: 'testuser.test.bspds.dev', 132 did, 133 didDoc: {}, 134 collections: ['app.bsky.feed.post', 'app.bsky.feed.like', 'app.bsky.graph.follow'], ··· 173 mockEndpoint('com.atproto.server.createInviteCode', () => 174 jsonResponse({ code: 'new-invite-' + Date.now() }) 175 ) 176 - mockEndpoint('com.bspds.account.getNotificationPrefs', () => 177 jsonResponse(mockData.notificationPrefs()) 178 ) 179 - mockEndpoint('com.bspds.account.updateNotificationPrefs', () => 180 jsonResponse({ success: true }) 181 ) 182 mockEndpoint('com.atproto.server.requestEmailUpdate', () =>
··· 85 } 86 export const mockData = { 87 session: (overrides?: Partial<Session>): Session => ({ 88 + did: 'did:web:test.tranquil.dev:u:testuser', 89 + handle: 'testuser.test.tranquil.dev', 90 email: 'test@example.com', 91 emailConfirmed: true, 92 accessJwt: 'mock-access-jwt-token', ··· 102 code: 'test-invite-123', 103 available: 1, 104 disabled: false, 105 + forAccount: 'did:web:test.tranquil.dev:u:testuser', 106 + createdBy: 'did:web:test.tranquil.dev:u:testuser', 107 createdAt: new Date().toISOString(), 108 uses: [], 109 ...overrides, ··· 120 ...overrides, 121 }), 122 describeServer: () => ({ 123 + availableUserDomains: ['test.tranquil.dev'], 124 inviteCodeRequired: false, 125 links: { 126 privacyPolicy: 'https://example.com/privacy', ··· 128 }, 129 }), 130 describeRepo: (did: string) => ({ 131 + handle: 'testuser.test.tranquil.dev', 132 did, 133 didDoc: {}, 134 collections: ['app.bsky.feed.post', 'app.bsky.feed.like', 'app.bsky.graph.follow'], ··· 173 mockEndpoint('com.atproto.server.createInviteCode', () => 174 jsonResponse({ code: 'new-invite-' + Date.now() }) 175 ) 176 + mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 177 jsonResponse(mockData.notificationPrefs()) 178 ) 179 + mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 180 jsonResponse({ success: true }) 181 ) 182 mockEndpoint('com.atproto.server.requestEmailUpdate', () =>
+1 -1
justfile
··· 25 ./scripts/run-tests.sh --test {{file}} 26 # Run tests with testcontainers (slower, no shared infra) 27 test-standalone: 28 - BSPDS_ALLOW_INSECURE_SECRETS=1 cargo test 29 # Manually manage test infrastructure (for debugging) 30 test-infra-start: 31 ./scripts/test-infra.sh start
··· 25 ./scripts/run-tests.sh --test {{file}} 26 # Run tests with testcontainers (slower, no shared infra) 27 test-standalone: 28 + TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 cargo test 29 # Manually manage test infrastructure (for debugging) 30 test-infra-start: 31 ./scripts/test-infra.sh start
+4 -4
nginx.prod.conf
··· 34 ssl_session_tickets off; 35 ssl_stapling on; 36 ssl_stapling_verify on; 37 - upstream bspds { 38 - server bspds:3000; 39 keepalive 32; 40 } 41 server { ··· 57 ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem; 58 client_max_body_size 100M; 59 location / { 60 - proxy_pass http://bspds; 61 proxy_http_version 1.1; 62 proxy_set_header Upgrade $http_upgrade; 63 proxy_set_header Connection "upgrade"; ··· 71 proxy_request_buffering off; 72 } 73 location /xrpc/com.atproto.sync.subscribeRepos { 74 - proxy_pass http://bspds; 75 proxy_http_version 1.1; 76 proxy_set_header Upgrade $http_upgrade; 77 proxy_set_header Connection "upgrade";
··· 34 ssl_session_tickets off; 35 ssl_stapling on; 36 ssl_stapling_verify on; 37 + upstream tranquil-pds { 38 + server tranquil-pds:3000; 39 keepalive 32; 40 } 41 server { ··· 57 ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem; 58 client_max_body_size 100M; 59 location / { 60 + proxy_pass http://tranquil-pds; 61 proxy_http_version 1.1; 62 proxy_set_header Upgrade $http_upgrade; 63 proxy_set_header Connection "upgrade"; ··· 71 proxy_request_buffering off; 72 } 73 location /xrpc/com.atproto.sync.subscribeRepos { 74 + proxy_pass http://tranquil-pds; 75 proxy_http_version 1.1; 76 proxy_set_header Upgrade $http_upgrade; 77 proxy_set_header Connection "upgrade";
+1 -1
observability/prometheus.yml
··· 7 static_configs: 8 - targets: ['localhost:9090'] 9 10 - - job_name: 'bspds' 11 static_configs: 12 - targets: ['app:3000'] 13 metrics_path: /metrics
··· 7 static_configs: 8 - targets: ['localhost:9090'] 9 10 + - job_name: 'tranquil-pds' 11 static_configs: 12 - targets: ['app:3000'] 13 metrics_path: /metrics
+75 -75
scripts/install-debian.sh
··· 24 nuke_installation() { 25 log_warn "NUKING EXISTING INSTALLATION" 26 log_info "Stopping services..." 27 - systemctl stop bspds 2>/dev/null || true 28 - systemctl disable bspds 2>/dev/null || true 29 30 - log_info "Removing BSPDS files..." 31 - rm -rf /opt/bspds 32 - rm -rf /var/lib/bspds 33 - rm -f /usr/local/bin/bspds 34 - rm -f /usr/local/bin/bspds-sendmail 35 - rm -f /usr/local/bin/bspds-mailq 36 - rm -rf /var/spool/bspds-mail 37 - rm -f /etc/systemd/system/bspds.service 38 systemctl daemon-reload 39 40 - log_info "Removing BSPDS configuration..." 41 - rm -rf /etc/bspds 42 43 log_info "Dropping postgres database and user..." 44 sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true 45 - sudo -u postgres psql -c "DROP USER IF EXISTS bspds;" 2>/dev/null || true 46 47 log_info "Removing minio bucket..." 48 if command -v mc &>/dev/null; then ··· 54 rm -f /etc/default/minio 2>/dev/null || true 55 56 log_info "Removing nginx config..." 57 - rm -f /etc/nginx/sites-enabled/bspds 58 - rm -f /etc/nginx/sites-available/bspds 59 systemctl reload nginx 2>/dev/null || true 60 61 log_success "Previous installation nuked" 62 } 63 64 - if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/bspds ]]; then 65 log_warn "Existing installation detected" 66 echo "" 67 echo "Options:" ··· 76 echo "" 77 log_warn "This will DELETE:" 78 echo " - PostgreSQL database 'pds' and all data" 79 - echo " - All BSPDS configuration and credentials" 80 - echo " - All source code in /opt/bspds" 81 echo " - MinIO bucket 'pds-blobs' and all blobs" 82 echo "" 83 read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE ··· 102 fi 103 104 echo "" 105 - log_info "BSPDS Installation Script for Debian" 106 echo "" 107 108 get_public_ips() { ··· 142 exit 0 143 fi 144 145 - CREDENTIALS_FILE="/etc/bspds/.credentials" 146 if [[ -f "$CREDENTIALS_FILE" ]]; then 147 log_info "Loading existing credentials..." 148 source "$CREDENTIALS_FILE" ··· 154 DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) 155 MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) 156 157 - mkdir -p /etc/bspds 158 cat > "$CREDENTIALS_FILE" << EOF 159 JWT_SECRET="$JWT_SECRET" 160 DPOP_SECRET="$DPOP_SECRET" ··· 196 apt install -y postgresql postgresql-contrib 197 systemctl enable postgresql 198 systemctl start postgresql 199 - sudo -u postgres psql -c "CREATE USER bspds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \ 200 - sudo -u postgres psql -c "ALTER USER bspds WITH PASSWORD '${DB_PASSWORD}';" 201 - sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 2>/dev/null || true 202 - sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;" 203 log_success "postgres configured" 204 205 log_info "Installing valkey..." ··· 292 grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 293 fi 294 295 - log_info "Cloning BSPDS..." 296 - if [[ ! -d /opt/bspds ]]; then 297 - git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/bspds 298 else 299 - cd /opt/bspds && git pull 300 fi 301 - cd /opt/bspds 302 303 log_info "Building frontend..." 304 "$HOME/.deno/bin/deno" task build --filter=frontend 305 log_success "Frontend built" 306 307 - log_info "Building BSPDS (this takes a while)..." 308 source "$HOME/.cargo/env" 309 if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then 310 log_info "Low memory - limiting parallel jobs" ··· 312 else 313 cargo build --release 314 fi 315 - log_success "BSPDS built" 316 317 log_info "Running migrations..." 318 cargo install sqlx-cli --no-default-features --features postgres 319 - export DATABASE_URL="postgres://bspds:${DB_PASSWORD}@localhost:5432/pds" 320 "$HOME/.cargo/bin/sqlx" migrate run 321 log_success "Migrations complete" 322 323 log_info "Setting up mail trap..." 324 - mkdir -p /var/spool/bspds-mail 325 - chmod 1777 /var/spool/bspds-mail 326 327 - cat > /usr/local/bin/bspds-sendmail << 'SENDMAIL_EOF' 328 #!/bin/bash 329 - MAIL_DIR="/var/spool/bspds-mail" 330 TIMESTAMP=$(date +%Y%m%d-%H%M%S) 331 RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p) 332 MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml" 333 mkdir -p "$MAIL_DIR" 334 { 335 - echo "X-BSPDS-Received: $(date -Iseconds)" 336 - echo "X-BSPDS-Args: $*" 337 echo "" 338 cat 339 } > "$MAIL_FILE" 340 chmod 644 "$MAIL_FILE" 341 exit 0 342 SENDMAIL_EOF 343 - chmod +x /usr/local/bin/bspds-sendmail 344 345 - cat > /usr/local/bin/bspds-mailq << 'MAILQ_EOF' 346 #!/bin/bash 347 - MAIL_DIR="/var/spool/bspds-mail" 348 case "${1:-list}" in 349 list) 350 ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails" ··· 365 [[ -f "$f" ]] && cat "$f" || echo "Not found" 366 ;; 367 *) 368 - [[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: bspds-mailq [list|latest|clear|count|N]" 369 ;; 370 esac 371 MAILQ_EOF 372 - chmod +x /usr/local/bin/bspds-mailq 373 374 - log_info "Creating BSPDS configuration..." 375 - cat > /etc/bspds/bspds.env << EOF 376 SERVER_HOST=127.0.0.1 377 SERVER_PORT=3000 378 PDS_HOSTNAME=${PDS_DOMAIN} 379 - DATABASE_URL=postgres://bspds:${DB_PASSWORD}@localhost:5432/pds 380 DATABASE_MAX_CONNECTIONS=100 381 DATABASE_MIN_CONNECTIONS=10 382 S3_ENDPOINT=http://localhost:9000 ··· 392 CRAWLERS=https://bsky.network 393 AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} 394 MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN} 395 - MAIL_FROM_NAME=BSPDS 396 - SENDMAIL_PATH=/usr/local/bin/bspds-sendmail 397 EOF 398 - chmod 600 /etc/bspds/bspds.env 399 400 - log_info "Installing BSPDS..." 401 - id -u bspds &>/dev/null || useradd -r -s /sbin/nologin bspds 402 - cp /opt/bspds/target/release/bspds /usr/local/bin/ 403 - mkdir -p /var/lib/bspds 404 - cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend 405 - chown -R bspds:bspds /var/lib/bspds 406 407 - cat > /etc/systemd/system/bspds.service << 'EOF' 408 [Unit] 409 - Description=BSPDS - AT Protocol PDS 410 After=network.target postgresql.service minio.service 411 412 [Service] 413 Type=simple 414 - User=bspds 415 - Group=bspds 416 - EnvironmentFile=/etc/bspds/bspds.env 417 - Environment=FRONTEND_DIR=/var/lib/bspds/frontend 418 - ExecStart=/usr/local/bin/bspds 419 Restart=always 420 RestartSec=5 421 ··· 424 EOF 425 426 systemctl daemon-reload 427 - systemctl enable bspds 428 - systemctl start bspds 429 - log_success "BSPDS service started" 430 431 log_info "Installing nginx..." 432 apt install -y nginx 433 - cat > /etc/nginx/sites-available/bspds << EOF 434 server { 435 listen 80; 436 listen [::]:80; ··· 456 } 457 EOF 458 459 - ln -sf /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/ 460 rm -f /etc/nginx/sites-enabled/default 461 nginx -t 462 systemctl reload nginx ··· 496 -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \ 497 --email "${CERTBOT_EMAIL}" --agree-tos; then 498 499 - cat > /etc/nginx/sites-available/bspds << EOF 500 server { 501 listen 80; 502 listen [::]:80; ··· 564 log_info "Verifying installation..." 565 sleep 3 566 if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then 567 - log_success "BSPDS is responding" 568 else 569 - log_warn "BSPDS may still be starting. Check: journalctl -u bspds -f" 570 fi 571 572 echo "" ··· 574 echo "" 575 echo "PDS: https://${PDS_DOMAIN}" 576 echo "" 577 - echo "Credentials (also in /etc/bspds/.credentials):" 578 echo " DB password: ${DB_PASSWORD}" 579 echo " MinIO password: ${MINIO_PASSWORD}" 580 echo "" 581 echo "Commands:" 582 - echo " journalctl -u bspds -f # logs" 583 - echo " systemctl restart bspds # restart" 584 - echo " bspds-mailq # view trapped emails" 585 echo ""
··· 24 nuke_installation() { 25 log_warn "NUKING EXISTING INSTALLATION" 26 log_info "Stopping services..." 27 + systemctl stop tranquil-pds 2>/dev/null || true 28 + systemctl disable tranquil-pds 2>/dev/null || true 29 30 + log_info "Removing Tranquil PDS files..." 31 + rm -rf /opt/tranquil-pds 32 + rm -rf /var/lib/tranquil-pds 33 + rm -f /usr/local/bin/tranquil-pds 34 + rm -f /usr/local/bin/tranquil-pds-sendmail 35 + rm -f /usr/local/bin/tranquil-pds-mailq 36 + rm -rf /var/spool/tranquil-pds-mail 37 + rm -f /etc/systemd/system/tranquil-pds.service 38 systemctl daemon-reload 39 40 + log_info "Removing Tranquil PDS configuration..." 41 + rm -rf /etc/tranquil-pds 42 43 log_info "Dropping postgres database and user..." 44 sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true 45 + sudo -u postgres psql -c "DROP USER IF EXISTS tranquil_pds;" 2>/dev/null || true 46 47 log_info "Removing minio bucket..." 48 if command -v mc &>/dev/null; then ··· 54 rm -f /etc/default/minio 2>/dev/null || true 55 56 log_info "Removing nginx config..." 57 + rm -f /etc/nginx/sites-enabled/tranquil-pds 58 + rm -f /etc/nginx/sites-available/tranquil-pds 59 systemctl reload nginx 2>/dev/null || true 60 61 log_success "Previous installation nuked" 62 } 63 64 + if [[ -f /etc/tranquil-pds/tranquil-pds.env ]] || [[ -d /opt/tranquil-pds ]] || [[ -f /usr/local/bin/tranquil-pds ]]; then 65 log_warn "Existing installation detected" 66 echo "" 67 echo "Options:" ··· 76 echo "" 77 log_warn "This will DELETE:" 78 echo " - PostgreSQL database 'pds' and all data" 79 + echo " - All Tranquil PDS configuration and credentials" 80 + echo " - All source code in /opt/tranquil-pds" 81 echo " - MinIO bucket 'pds-blobs' and all blobs" 82 echo "" 83 read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE ··· 102 fi 103 104 echo "" 105 + log_info "Tranquil PDS Installation Script for Debian" 106 echo "" 107 108 get_public_ips() { ··· 142 exit 0 143 fi 144 145 + CREDENTIALS_FILE="/etc/tranquil-pds/.credentials" 146 if [[ -f "$CREDENTIALS_FILE" ]]; then 147 log_info "Loading existing credentials..." 148 source "$CREDENTIALS_FILE" ··· 154 DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) 155 MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) 156 157 + mkdir -p /etc/tranquil-pds 158 cat > "$CREDENTIALS_FILE" << EOF 159 JWT_SECRET="$JWT_SECRET" 160 DPOP_SECRET="$DPOP_SECRET" ··· 196 apt install -y postgresql postgresql-contrib 197 systemctl enable postgresql 198 systemctl start postgresql 199 + sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \ 200 + sudo -u postgres psql -c "ALTER USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';" 201 + sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 2>/dev/null || true 202 + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 203 log_success "postgres configured" 204 205 log_info "Installing valkey..." ··· 292 grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 293 fi 294 295 + log_info "Cloning Tranquil PDS..." 296 + if [[ ! -d /opt/tranquil-pds ]]; then 297 + git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/tranquil-pds 298 else 299 + cd /opt/tranquil-pds && git pull 300 fi 301 + cd /opt/tranquil-pds 302 303 log_info "Building frontend..." 304 "$HOME/.deno/bin/deno" task build --filter=frontend 305 log_success "Frontend built" 306 307 + log_info "Building Tranquil PDS (this takes a while)..." 308 source "$HOME/.cargo/env" 309 if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then 310 log_info "Low memory - limiting parallel jobs" ··· 312 else 313 cargo build --release 314 fi 315 + log_success "Tranquil PDS built" 316 317 log_info "Running migrations..." 318 cargo install sqlx-cli --no-default-features --features postgres 319 + export DATABASE_URL="postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds" 320 "$HOME/.cargo/bin/sqlx" migrate run 321 log_success "Migrations complete" 322 323 log_info "Setting up mail trap..." 324 + mkdir -p /var/spool/tranquil-pds-mail 325 + chmod 1777 /var/spool/tranquil-pds-mail 326 327 + cat > /usr/local/bin/tranquil-pds-sendmail << 'SENDMAIL_EOF' 328 #!/bin/bash 329 + MAIL_DIR="/var/spool/tranquil-pds-mail" 330 TIMESTAMP=$(date +%Y%m%d-%H%M%S) 331 RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p) 332 MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml" 333 mkdir -p "$MAIL_DIR" 334 { 335 + echo "X-Tranquil-PDS-Received: $(date -Iseconds)" 336 + echo "X-Tranquil-PDS-Args: $*" 337 echo "" 338 cat 339 } > "$MAIL_FILE" 340 chmod 644 "$MAIL_FILE" 341 exit 0 342 SENDMAIL_EOF 343 + chmod +x /usr/local/bin/tranquil-pds-sendmail 344 345 + cat > /usr/local/bin/tranquil-pds-mailq << 'MAILQ_EOF' 346 #!/bin/bash 347 + MAIL_DIR="/var/spool/tranquil-pds-mail" 348 case "${1:-list}" in 349 list) 350 ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails" ··· 365 [[ -f "$f" ]] && cat "$f" || echo "Not found" 366 ;; 367 *) 368 + [[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: tranquil-pds-mailq [list|latest|clear|count|N]" 369 ;; 370 esac 371 MAILQ_EOF 372 + chmod +x /usr/local/bin/tranquil-pds-mailq 373 374 + log_info "Creating Tranquil PDS configuration..." 375 + cat > /etc/tranquil-pds/tranquil-pds.env << EOF 376 SERVER_HOST=127.0.0.1 377 SERVER_PORT=3000 378 PDS_HOSTNAME=${PDS_DOMAIN} 379 + DATABASE_URL=postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds 380 DATABASE_MAX_CONNECTIONS=100 381 DATABASE_MIN_CONNECTIONS=10 382 S3_ENDPOINT=http://localhost:9000 ··· 392 CRAWLERS=https://bsky.network 393 AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} 394 MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN} 395 + MAIL_FROM_NAME=Tranquil PDS 396 + SENDMAIL_PATH=/usr/local/bin/tranquil-pds-sendmail 397 EOF 398 + chmod 600 /etc/tranquil-pds/tranquil-pds.env 399 400 + log_info "Installing Tranquil PDS..." 401 + id -u tranquil-pds &>/dev/null || useradd -r -s /sbin/nologin tranquil-pds 402 + cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 403 + mkdir -p /var/lib/tranquil-pds 404 + cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend 405 + chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds 406 407 + cat > /etc/systemd/system/tranquil-pds.service << 'EOF' 408 [Unit] 409 + Description=Tranquil PDS - AT Protocol PDS 410 After=network.target postgresql.service minio.service 411 412 [Service] 413 Type=simple 414 + User=tranquil-pds 415 + Group=tranquil-pds 416 + EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 417 + Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 418 + ExecStart=/usr/local/bin/tranquil-pds 419 Restart=always 420 RestartSec=5 421 ··· 424 EOF 425 426 systemctl daemon-reload 427 + systemctl enable tranquil-pds 428 + systemctl start tranquil-pds 429 + log_success "Tranquil PDS service started" 430 431 log_info "Installing nginx..." 432 apt install -y nginx 433 + cat > /etc/nginx/sites-available/tranquil-pds << EOF 434 server { 435 listen 80; 436 listen [::]:80; ··· 456 } 457 EOF 458 459 + ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 460 rm -f /etc/nginx/sites-enabled/default 461 nginx -t 462 systemctl reload nginx ··· 496 -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \ 497 --email "${CERTBOT_EMAIL}" --agree-tos; then 498 499 + cat > /etc/nginx/sites-available/tranquil-pds << EOF 500 server { 501 listen 80; 502 listen [::]:80; ··· 564 log_info "Verifying installation..." 565 sleep 3 566 if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then 567 + log_success "Tranquil PDS is responding" 568 else 569 + log_warn "Tranquil PDS may still be starting. Check: journalctl -u tranquil-pds -f" 570 fi 571 572 echo "" ··· 574 echo "" 575 echo "PDS: https://${PDS_DOMAIN}" 576 echo "" 577 + echo "Credentials (also in /etc/tranquil-pds/.credentials):" 578 echo " DB password: ${DB_PASSWORD}" 579 echo " MinIO password: ${MINIO_PASSWORD}" 580 echo "" 581 echo "Commands:" 582 + echo " journalctl -u tranquil-pds -f # logs" 583 + echo " systemctl restart tranquil-pds # restart" 584 + echo " tranquil-pds-mailq # view trapped emails" 585 echo ""
+1 -1
scripts/run-tests.sh
··· 10 } 11 trap cleanup EXIT 12 "$INFRA_SCRIPT" start 13 - source "${TMPDIR:-/tmp}/bspds_test_infra.env" 14 echo "" 15 echo "Running database migrations..." 16 sqlx database create 2>/dev/null || true
··· 10 } 11 trap cleanup EXIT 12 "$INFRA_SCRIPT" start 13 + source "${TMPDIR:-/tmp}/tranquil_pds_test_infra.env" 14 echo "" 15 echo "Running database migrations..." 16 sqlx database create 2>/dev/null || true
+8 -8
scripts/test-infra.sh
··· 1 #!/usr/bin/env bash 2 set -euo pipefail 3 - INFRA_FILE="${TMPDIR:-/tmp}/bspds_test_infra.env" 4 - CONTAINER_PREFIX="bspds-test" 5 command_exists() { 6 command -v "$1" >/dev/null 2>&1 7 } ··· 40 -e POSTGRES_USER=postgres \ 41 -e POSTGRES_DB=postgres \ 42 -P \ 43 - --label bspds_test=true \ 44 postgres:18-alpine >/dev/null 45 echo "Starting MinIO..." 46 $CONTAINER_CMD run -d \ ··· 48 -e MINIO_ROOT_USER=minioadmin \ 49 -e MINIO_ROOT_PASSWORD=minioadmin \ 50 -P \ 51 - --label bspds_test=true \ 52 minio/minio:latest server /data >/dev/null 53 echo "Starting Valkey..." 54 $CONTAINER_CMD run -d \ 55 --name "${CONTAINER_PREFIX}-valkey" \ 56 -P \ 57 - --label bspds_test=true \ 58 valkey/valkey:8-alpine >/dev/null 59 echo "Waiting for services to be ready..." 60 sleep 2 ··· 95 export AWS_SECRET_ACCESS_KEY="minioadmin" 96 export AWS_REGION="us-east-1" 97 export VALKEY_URL="redis://127.0.0.1:${VALKEY_PORT}" 98 - export BSPDS_TEST_INFRA_READY="1" 99 - export BSPDS_ALLOW_INSECURE_SECRETS="1" 100 export SKIP_IMPORT_VERIFICATION="true" 101 export DISABLE_RATE_LIMITING="1" 102 EOF ··· 125 fi 126 echo "" 127 echo "Containers:" 128 - $CONTAINER_CMD ps -a --filter "label=bspds_test=true" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo " (none)" 129 } 130 case "${1:-}" in 131 start)
··· 1 #!/usr/bin/env bash 2 set -euo pipefail 3 + INFRA_FILE="${TMPDIR:-/tmp}/tranquil_pds_test_infra.env" 4 + CONTAINER_PREFIX="tranquil-pds-test" 5 command_exists() { 6 command -v "$1" >/dev/null 2>&1 7 } ··· 40 -e POSTGRES_USER=postgres \ 41 -e POSTGRES_DB=postgres \ 42 -P \ 43 + --label tranquil_pds_test=true \ 44 postgres:18-alpine >/dev/null 45 echo "Starting MinIO..." 46 $CONTAINER_CMD run -d \ ··· 48 -e MINIO_ROOT_USER=minioadmin \ 49 -e MINIO_ROOT_PASSWORD=minioadmin \ 50 -P \ 51 + --label tranquil_pds_test=true \ 52 minio/minio:latest server /data >/dev/null 53 echo "Starting Valkey..." 54 $CONTAINER_CMD run -d \ 55 --name "${CONTAINER_PREFIX}-valkey" \ 56 -P \ 57 + --label tranquil_pds_test=true \ 58 valkey/valkey:8-alpine >/dev/null 59 echo "Waiting for services to be ready..." 60 sleep 2 ··· 95 export AWS_SECRET_ACCESS_KEY="minioadmin" 96 export AWS_REGION="us-east-1" 97 export VALKEY_URL="redis://127.0.0.1:${VALKEY_PORT}" 98 + export TRANQUIL_PDS_TEST_INFRA_READY="1" 99 + export TRANQUIL_PDS_ALLOW_INSECURE_SECRETS="1" 100 export SKIP_IMPORT_VERIFICATION="true" 101 export DISABLE_RATE_LIMITING="1" 102 EOF ··· 125 fi 126 echo "" 127 echo "Containers:" 128 + $CONTAINER_CMD ps -a --filter "label=tranquil_pds_test=true" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo " (none)" 129 } 130 case "${1:-}" in 131 start)
+18 -2
src/api/server/account_status.rs
··· 157 .await; 158 match result { 159 Ok(_) => { 160 - if let Some(h) = handle { 161 let _ = state.cache.delete(&format!("handle:{}", h)).await; 162 } 163 (StatusCode::OK, Json(json!({}))).into_response() 164 } ··· 222 .await; 223 match result { 224 Ok(_) => { 225 - if let Some(h) = handle { 226 let _ = state.cache.delete(&format!("handle:{}", h)).await; 227 } 228 (StatusCode::OK, Json(json!({}))).into_response() 229 }
··· 157 .await; 158 match result { 159 Ok(_) => { 160 + if let Some(ref h) = handle { 161 let _ = state.cache.delete(&format!("handle:{}", h)).await; 162 + } 163 + if let Err(e) = 164 + crate::api::repo::record::sequence_account_event(&state, &did, true, None).await 165 + { 166 + warn!("Failed to sequence account activation event: {}", e); 167 + } 168 + if let Err(e) = 169 + crate::api::repo::record::sequence_identity_event(&state, &did, handle.as_deref()) 170 + .await 171 + { 172 + warn!("Failed to sequence identity event for activation: {}", e); 173 } 174 (StatusCode::OK, Json(json!({}))).into_response() 175 } ··· 233 .await; 234 match result { 235 Ok(_) => { 236 + if let Some(ref h) = handle { 237 let _ = state.cache.delete(&format!("handle:{}", h)).await; 238 + } 239 + if let Err(e) = 240 + crate::api::repo::record::sequence_account_event(&state, &did, false, Some("deactivated")).await 241 + { 242 + warn!("Failed to sequence account deactivation event: {}", e); 243 } 244 (StatusCode::OK, Json(json!({}))).into_response() 245 }
+102 -3
src/api/server/service_auth.rs
··· 10 use serde_json::json; 11 use tracing::error; 12 13 #[derive(Deserialize)] 14 pub struct GetServiceAuthParams { 15 pub aud: String, ··· 33 Some(t) => t, 34 None => return ApiError::AuthenticationRequired.into_response(), 35 }; 36 - let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await { 37 Ok(user) => user, 38 Err(e) => return ApiError::from(e).into_response(), 39 }; ··· 46 .into_response(); 47 } 48 }; 49 - let lxm = params.lxm.as_deref().unwrap_or("*"); 50 let service_token = 51 - match crate::auth::create_service_token(&auth_user.did, &params.aud, lxm, &key_bytes) { 52 Ok(t) => t, 53 Err(e) => { 54 error!("Failed to create service token: {:?}", e);
··· 10 use serde_json::json; 11 use tracing::error; 12 13 + const HOUR_SECS: i64 = 3600; 14 + const MINUTE_SECS: i64 = 60; 15 + 16 + const PROTECTED_METHODS: &[&str] = &[ 17 + "com.atproto.admin.sendEmail", 18 + "com.atproto.identity.requestPlcOperationSignature", 19 + "com.atproto.identity.signPlcOperation", 20 + "com.atproto.identity.updateHandle", 21 + "com.atproto.server.activateAccount", 22 + "com.atproto.server.confirmEmail", 23 + "com.atproto.server.createAppPassword", 24 + "com.atproto.server.deactivateAccount", 25 + "com.atproto.server.getAccountInviteCodes", 26 + "com.atproto.server.getSession", 27 + "com.atproto.server.listAppPasswords", 28 + "com.atproto.server.requestAccountDelete", 29 + "com.atproto.server.requestEmailConfirmation", 30 + "com.atproto.server.requestEmailUpdate", 31 + "com.atproto.server.revokeAppPassword", 32 + "com.atproto.server.updateEmail", 33 + ]; 34 + 35 #[derive(Deserialize)] 36 pub struct GetServiceAuthParams { 37 pub aud: String, ··· 55 Some(t) => t, 56 None => return ApiError::AuthenticationRequired.into_response(), 57 }; 58 + let auth_user = match crate::auth::validate_bearer_token_for_service_auth(&state.db, &token).await { 59 Ok(user) => user, 60 Err(e) => return ApiError::from(e).into_response(), 61 }; ··· 68 .into_response(); 69 } 70 }; 71 + 72 + let lxm = params.lxm.as_deref(); 73 + let lxm_for_token = lxm.unwrap_or("*"); 74 + 75 + let user_status = sqlx::query!( 76 + "SELECT takedown_ref FROM users WHERE did = $1", 77 + auth_user.did 78 + ) 79 + .fetch_optional(&state.db) 80 + .await; 81 + 82 + let is_takendown = match user_status { 83 + Ok(Some(row)) => row.takedown_ref.is_some(), 84 + _ => false, 85 + }; 86 + 87 + if is_takendown && lxm != Some("com.atproto.server.createAccount") { 88 + return ( 89 + StatusCode::BAD_REQUEST, 90 + Json(json!({ 91 + "error": "InvalidToken", 92 + "message": "Bad token scope" 93 + })), 94 + ) 95 + .into_response(); 96 + } 97 + 98 + if let Some(method) = lxm { 99 + if PROTECTED_METHODS.contains(&method) { 100 + return ( 101 + StatusCode::BAD_REQUEST, 102 + Json(json!({ 103 + "error": "InvalidRequest", 104 + "message": format!("cannot request a service auth token for the following protected method: {}", method) 105 + })), 106 + ) 107 + .into_response(); 108 + } 109 + } 110 + 111 + if let Some(exp) = params.exp { 112 + let now = chrono::Utc::now().timestamp(); 113 + let diff = exp - now; 114 + 115 + if diff < 0 { 116 + return ( 117 + StatusCode::BAD_REQUEST, 118 + Json(json!({ 119 + "error": "BadExpiration", 120 + "message": "expiration is in past" 121 + })), 122 + ) 123 + .into_response(); 124 + } 125 + 126 + if diff > HOUR_SECS { 127 + return ( 128 + StatusCode::BAD_REQUEST, 129 + Json(json!({ 130 + "error": "BadExpiration", 131 + "message": "cannot request a token with an expiration more than an hour in the future" 132 + })), 133 + ) 134 + .into_response(); 135 + } 136 + 137 + if lxm.is_none() && diff > MINUTE_SECS { 138 + return ( 139 + StatusCode::BAD_REQUEST, 140 + Json(json!({ 141 + "error": "BadExpiration", 142 + "message": "cannot request a method-less token with an expiration more than a minute in the future" 143 + })), 144 + ) 145 + .into_response(); 146 + } 147 + } 148 + 149 let service_token = 150 + match crate::auth::create_service_token(&auth_user.did, &params.aud, lxm_for_token, &key_bytes) { 151 Ok(t) => t, 152 Err(e) => { 153 error!("Failed to create service token: {:?}", e);
+13 -5
src/auth/mod.rs
··· 59 db: &PgPool, 60 token: &str, 61 ) -> Result<AuthenticatedUser, TokenValidationError> { 62 - validate_bearer_token_with_options_internal(db, None, token, false).await 63 } 64 65 pub async fn validate_bearer_token_allow_deactivated( 66 db: &PgPool, 67 token: &str, 68 ) -> Result<AuthenticatedUser, TokenValidationError> { 69 - validate_bearer_token_with_options_internal(db, None, token, true).await 70 } 71 72 pub async fn validate_bearer_token_cached( ··· 74 cache: &Arc<dyn Cache>, 75 token: &str, 76 ) -> Result<AuthenticatedUser, TokenValidationError> { 77 - validate_bearer_token_with_options_internal(db, Some(cache), token, false).await 78 } 79 80 pub async fn validate_bearer_token_cached_allow_deactivated( ··· 82 cache: &Arc<dyn Cache>, 83 token: &str, 84 ) -> Result<AuthenticatedUser, TokenValidationError> { 85 - validate_bearer_token_with_options_internal(db, Some(cache), token, true).await 86 } 87 88 async fn validate_bearer_token_with_options_internal( ··· 90 cache: Option<&Arc<dyn Cache>>, 91 token: &str, 92 allow_deactivated: bool, 93 ) -> Result<AuthenticatedUser, TokenValidationError> { 94 let did_from_token = get_did_from_token(token).ok(); 95 ··· 155 return Err(TokenValidationError::AccountDeactivated); 156 } 157 158 - if takedown_ref.is_some() { 159 return Err(TokenValidationError::AccountTakedown); 160 } 161
··· 59 db: &PgPool, 60 token: &str, 61 ) -> Result<AuthenticatedUser, TokenValidationError> { 62 + validate_bearer_token_with_options_internal(db, None, token, false, false).await 63 } 64 65 pub async fn validate_bearer_token_allow_deactivated( 66 db: &PgPool, 67 token: &str, 68 ) -> Result<AuthenticatedUser, TokenValidationError> { 69 + validate_bearer_token_with_options_internal(db, None, token, true, false).await 70 } 71 72 pub async fn validate_bearer_token_cached( ··· 74 cache: &Arc<dyn Cache>, 75 token: &str, 76 ) -> Result<AuthenticatedUser, TokenValidationError> { 77 + validate_bearer_token_with_options_internal(db, Some(cache), token, false, false).await 78 } 79 80 pub async fn validate_bearer_token_cached_allow_deactivated( ··· 82 cache: &Arc<dyn Cache>, 83 token: &str, 84 ) -> Result<AuthenticatedUser, TokenValidationError> { 85 + validate_bearer_token_with_options_internal(db, Some(cache), token, true, false).await 86 + } 87 + 88 + pub async fn validate_bearer_token_for_service_auth( 89 + db: &PgPool, 90 + token: &str, 91 + ) -> Result<AuthenticatedUser, TokenValidationError> { 92 + validate_bearer_token_with_options_internal(db, None, token, true, true).await 93 } 94 95 async fn validate_bearer_token_with_options_internal( ··· 97 cache: Option<&Arc<dyn Cache>>, 98 token: &str, 99 allow_deactivated: bool, 100 + allow_takendown: bool, 101 ) -> Result<AuthenticatedUser, TokenValidationError> { 102 let did_from_token = get_did_from_token(token).ok(); 103 ··· 163 return Err(TokenValidationError::AccountDeactivated); 164 } 165 166 + if !allow_takendown && takedown_ref.is_some() { 167 return Err(TokenValidationError::AccountTakedown); 168 } 169
+2 -2
src/comms/sender.rs
··· 87 88 pub fn from_env() -> Option<Self> { 89 let from_address = std::env::var("MAIL_FROM_ADDRESS").ok()?; 90 - let from_name = std::env::var("MAIL_FROM_NAME").unwrap_or_else(|_| "BSPDS".to_string()); 91 Some(Self::new(from_address, from_name)) 92 } 93 ··· 168 let content = format!("**{}**\n\n{}", subject, notification.body); 169 let payload = json!({ 170 "content": content, 171 - "username": "BSPDS" 172 }); 173 let mut last_error = None; 174 for attempt in 0..MAX_RETRIES {
··· 87 88 pub fn from_env() -> Option<Self> { 89 let from_address = std::env::var("MAIL_FROM_ADDRESS").ok()?; 90 + let from_name = std::env::var("MAIL_FROM_NAME").unwrap_or_else(|_| "Tranquil PDS".to_string()); 91 Some(Self::new(from_address, from_name)) 92 } 93 ··· 168 let content = format!("**{}**\n\n{}", subject, notification.body); 169 let payload = json!({ 170 "content": content, 171 + "username": "Tranquil PDS" 172 }); 173 let mut last_error = None; 174 for attempt in 0..MAX_RETRIES {
+10 -10
src/config.rs
··· 25 pub fn init() -> &'static Self { 26 CONFIG.get_or_init(|| { 27 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| { 28 - if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() { 29 "test-jwt-secret-not-for-production".to_string() 30 } else { 31 panic!( 32 "JWT_SECRET environment variable must be set in production. \ 33 - Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 34 ); 35 } 36 }); 37 38 let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| { 39 - if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() { 40 "test-dpop-secret-not-for-production".to_string() 41 } else { 42 panic!( 43 "DPOP_SECRET environment variable must be set in production. \ 44 - Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 45 ); 46 } 47 }); 48 49 - if jwt_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() { 50 panic!("JWT_SECRET must be at least 32 characters"); 51 } 52 53 - if dpop_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() { 54 panic!("DPOP_SECRET must be at least 32 characters"); 55 } 56 ··· 87 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]); 88 89 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| { 90 - if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() { 91 "test-master-key-not-for-production".to_string() 92 } else { 93 panic!( 94 "MASTER_KEY environment variable must be set in production. \ 95 - Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 96 ); 97 } 98 }); 99 100 - if master_key.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() { 101 panic!("MASTER_KEY must be at least 32 characters"); 102 } 103 104 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes()); 105 let mut key_encryption_key = [0u8; 32]; 106 - hk.expand(b"bspds-user-key-encryption", &mut key_encryption_key) 107 .expect("HKDF expansion failed"); 108 109 AuthConfig {
··· 25 pub fn init() -> &'static Self { 26 CONFIG.get_or_init(|| { 27 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| { 28 + if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 29 "test-jwt-secret-not-for-production".to_string() 30 } else { 31 panic!( 32 "JWT_SECRET environment variable must be set in production. \ 33 + Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 34 ); 35 } 36 }); 37 38 let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| { 39 + if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 40 "test-dpop-secret-not-for-production".to_string() 41 } else { 42 panic!( 43 "DPOP_SECRET environment variable must be set in production. \ 44 + Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 45 ); 46 } 47 }); 48 49 + if jwt_secret.len() < 32 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() { 50 panic!("JWT_SECRET must be at least 32 characters"); 51 } 52 53 + if dpop_secret.len() < 32 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() { 54 panic!("DPOP_SECRET must be at least 32 characters"); 55 } 56 ··· 87 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]); 88 89 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| { 90 + if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 91 "test-master-key-not-for-production".to_string() 92 } else { 93 panic!( 94 "MASTER_KEY environment variable must be set in production. \ 95 + Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 96 ); 97 } 98 }); 99 100 + if master_key.len() < 32 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() { 101 panic!("MASTER_KEY must be at least 32 characters"); 102 } 103 104 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes()); 105 let mut key_encryption_key = [0u8; 32]; 106 + hk.expand(b"tranquil-pds-user-key-encryption", &mut key_encryption_key) 107 .expect("HKDF expansion failed"); 108 109 AuthConfig {
+8 -8
src/lib.rs
··· 52 get(api::server::get_session), 53 ) 54 .route( 55 - "/xrpc/com.bspds.account.listSessions", 56 get(api::server::list_sessions), 57 ) 58 .route( 59 - "/xrpc/com.bspds.account.revokeSession", 60 post(api::server::revoke_session), 61 ) 62 .route( ··· 199 post(api::server::reset_password), 200 ) 201 .route( 202 - "/xrpc/com.bspds.account.changePassword", 203 post(api::server::change_password), 204 ) 205 .route( ··· 283 get(api::admin::get_invite_codes), 284 ) 285 .route( 286 - "/xrpc/com.bspds.admin.getServerStats", 287 get(api::admin::get_server_stats), 288 ) 289 .route( ··· 370 get(api::temp::check_signup_queue), 371 ) 372 .route( 373 - "/xrpc/com.bspds.account.getNotificationPrefs", 374 get(api::notification_prefs::get_notification_prefs), 375 ) 376 .route( 377 - "/xrpc/com.bspds.account.updateNotificationPrefs", 378 post(api::notification_prefs::update_notification_prefs), 379 ) 380 .route( 381 - "/xrpc/com.bspds.account.getNotificationHistory", 382 get(api::notification_prefs::get_notification_history), 383 ) 384 .route( 385 - "/xrpc/com.bspds.account.confirmChannelVerification", 386 post(api::verification::confirm_channel_verification), 387 ) 388 .route("/xrpc/{*method}", any(api::proxy::proxy_handler))
··· 52 get(api::server::get_session), 53 ) 54 .route( 55 + "/xrpc/com.tranquil.account.listSessions", 56 get(api::server::list_sessions), 57 ) 58 .route( 59 + "/xrpc/com.tranquil.account.revokeSession", 60 post(api::server::revoke_session), 61 ) 62 .route( ··· 199 post(api::server::reset_password), 200 ) 201 .route( 202 + "/xrpc/com.tranquil.account.changePassword", 203 post(api::server::change_password), 204 ) 205 .route( ··· 283 get(api::admin::get_invite_codes), 284 ) 285 .route( 286 + "/xrpc/com.tranquil.admin.getServerStats", 287 get(api::admin::get_server_stats), 288 ) 289 .route( ··· 370 get(api::temp::check_signup_queue), 371 ) 372 .route( 373 + "/xrpc/com.tranquil.account.getNotificationPrefs", 374 get(api::notification_prefs::get_notification_prefs), 375 ) 376 .route( 377 + "/xrpc/com.tranquil.account.updateNotificationPrefs", 378 post(api::notification_prefs::update_notification_prefs), 379 ) 380 .route( 381 + "/xrpc/com.tranquil.account.getNotificationHistory", 382 get(api::notification_prefs::get_notification_history), 383 ) 384 .route( 385 + "/xrpc/com.tranquil.account.confirmChannelVerification", 386 post(api::verification::confirm_channel_verification), 387 ) 388 .route("/xrpc/{*method}", any(api::proxy::proxy_handler))
+6 -6
src/main.rs
··· 1 - use bspds::comms::{CommsService, DiscordSender, EmailSender, SignalSender, TelegramSender}; 2 - use bspds::crawlers::{Crawlers, start_crawlers_service}; 3 - use bspds::state::AppState; 4 use std::net::SocketAddr; 5 use std::process::ExitCode; 6 use std::sync::Arc; ··· 11 async fn main() -> ExitCode { 12 dotenvy::dotenv().ok(); 13 tracing_subscriber::fmt::init(); 14 - bspds::metrics::init_metrics(); 15 16 match run().await { 17 Ok(()) => ExitCode::SUCCESS, ··· 62 .map_err(|e| format!("Failed to run migrations: {}", e))?; 63 64 let state = AppState::new(pool.clone()).await; 65 - bspds::sync::listener::start_sequencer_listener(state.clone()).await; 66 67 let (shutdown_tx, shutdown_rx) = watch::channel(false); 68 ··· 108 None 109 }; 110 111 - let app = bspds::app(state); 112 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 113 info!("listening on {}", addr); 114
··· 1 + use tranquil_pds::comms::{CommsService, DiscordSender, EmailSender, SignalSender, TelegramSender}; 2 + use tranquil_pds::crawlers::{Crawlers, start_crawlers_service}; 3 + use tranquil_pds::state::AppState; 4 use std::net::SocketAddr; 5 use std::process::ExitCode; 6 use std::sync::Arc; ··· 11 async fn main() -> ExitCode { 12 dotenvy::dotenv().ok(); 13 tracing_subscriber::fmt::init(); 14 + tranquil_pds::metrics::init_metrics(); 15 16 match run().await { 17 Ok(()) => ExitCode::SUCCESS, ··· 62 .map_err(|e| format!("Failed to run migrations: {}", e))?; 63 64 let state = AppState::new(pool.clone()).await; 65 + tranquil_pds::sync::listener::start_sequencer_listener(state.clone()).await; 66 67 let (shutdown_tx, shutdown_rx) = watch::channel(false); 68 ··· 108 None 109 }; 110 111 + let app = tranquil_pds::app(state); 112 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 113 info!("listening on {}", addr); 114
+25 -25
src/metrics.rs
··· 24 } 25 26 fn describe_metrics() { 27 - metrics::describe_counter!("bspds_http_requests_total", "Total number of HTTP requests"); 28 metrics::describe_histogram!( 29 - "bspds_http_request_duration_seconds", 30 "HTTP request duration in seconds" 31 ); 32 metrics::describe_counter!( 33 - "bspds_auth_cache_hits_total", 34 "Total number of authentication cache hits" 35 ); 36 metrics::describe_counter!( 37 - "bspds_auth_cache_misses_total", 38 "Total number of authentication cache misses" 39 ); 40 metrics::describe_gauge!( 41 - "bspds_firehose_subscribers", 42 "Number of active firehose WebSocket subscribers" 43 ); 44 metrics::describe_counter!( 45 - "bspds_firehose_events_total", 46 "Total number of firehose events published" 47 ); 48 metrics::describe_counter!( 49 - "bspds_block_operations_total", 50 "Total number of block store operations" 51 ); 52 metrics::describe_counter!( 53 - "bspds_s3_operations_total", 54 "Total number of S3/blob storage operations" 55 ); 56 metrics::describe_gauge!( 57 - "bspds_comms_queue_size", 58 "Current size of the comms queue" 59 ); 60 metrics::describe_counter!( 61 - "bspds_rate_limit_rejections_total", 62 "Total number of rate limit rejections" 63 ); 64 - metrics::describe_counter!("bspds_db_queries_total", "Total number of database queries"); 65 metrics::describe_histogram!( 66 - "bspds_db_query_duration_seconds", 67 "Database query duration in seconds" 68 ); 69 } ··· 97 let status = response.status().as_u16().to_string(); 98 99 counter!( 100 - "bspds_http_requests_total", 101 "method" => method.clone(), 102 "path" => path.clone(), 103 "status" => status.clone() ··· 105 .increment(1); 106 107 histogram!( 108 - "bspds_http_request_duration_seconds", 109 "method" => method, 110 "path" => path 111 ) ··· 135 } 136 137 pub fn record_auth_cache_hit(cache_type: &str) { 138 - counter!("bspds_auth_cache_hits_total", "cache_type" => cache_type.to_string()).increment(1); 139 } 140 141 pub fn record_auth_cache_miss(cache_type: &str) { 142 - counter!("bspds_auth_cache_misses_total", "cache_type" => cache_type.to_string()).increment(1); 143 } 144 145 pub fn set_firehose_subscribers(count: usize) { 146 - gauge!("bspds_firehose_subscribers").set(count as f64); 147 } 148 149 pub fn increment_firehose_subscribers() { 150 - counter!("bspds_firehose_events_total").increment(1); 151 } 152 153 pub fn record_firehose_event() { 154 - counter!("bspds_firehose_events_total").increment(1); 155 } 156 157 pub fn record_block_operation(op_type: &str) { 158 - counter!("bspds_block_operations_total", "op_type" => op_type.to_string()).increment(1); 159 } 160 161 pub fn record_s3_operation(op_type: &str, status: &str) { 162 counter!( 163 - "bspds_s3_operations_total", 164 "op_type" => op_type.to_string(), 165 "status" => status.to_string() 166 ) ··· 168 } 169 170 pub fn set_comms_queue_size(size: usize) { 171 - gauge!("bspds_comms_queue_size").set(size as f64); 172 } 173 174 pub fn record_rate_limit_rejection(limiter: &str) { 175 - counter!("bspds_rate_limit_rejections_total", "limiter" => limiter.to_string()).increment(1); 176 } 177 178 pub fn record_db_query(query_type: &str, duration_seconds: f64) { 179 - counter!("bspds_db_queries_total", "query_type" => query_type.to_string()).increment(1); 180 histogram!( 181 - "bspds_db_query_duration_seconds", 182 "query_type" => query_type.to_string() 183 ) 184 .record(duration_seconds);
··· 24 } 25 26 fn describe_metrics() { 27 + metrics::describe_counter!("tranquil_pds_http_requests_total", "Total number of HTTP requests"); 28 metrics::describe_histogram!( 29 + "tranquil_pds_http_request_duration_seconds", 30 "HTTP request duration in seconds" 31 ); 32 metrics::describe_counter!( 33 + "tranquil_pds_auth_cache_hits_total", 34 "Total number of authentication cache hits" 35 ); 36 metrics::describe_counter!( 37 + "tranquil_pds_auth_cache_misses_total", 38 "Total number of authentication cache misses" 39 ); 40 metrics::describe_gauge!( 41 + "tranquil_pds_firehose_subscribers", 42 "Number of active firehose WebSocket subscribers" 43 ); 44 metrics::describe_counter!( 45 + "tranquil_pds_firehose_events_total", 46 "Total number of firehose events published" 47 ); 48 metrics::describe_counter!( 49 + "tranquil_pds_block_operations_total", 50 "Total number of block store operations" 51 ); 52 metrics::describe_counter!( 53 + "tranquil_pds_s3_operations_total", 54 "Total number of S3/blob storage operations" 55 ); 56 metrics::describe_gauge!( 57 + "tranquil_pds_comms_queue_size", 58 "Current size of the comms queue" 59 ); 60 metrics::describe_counter!( 61 + "tranquil_pds_rate_limit_rejections_total", 62 "Total number of rate limit rejections" 63 ); 64 + metrics::describe_counter!("tranquil_pds_db_queries_total", "Total number of database queries"); 65 metrics::describe_histogram!( 66 + "tranquil_pds_db_query_duration_seconds", 67 "Database query duration in seconds" 68 ); 69 } ··· 97 let status = response.status().as_u16().to_string(); 98 99 counter!( 100 + "tranquil_pds_http_requests_total", 101 "method" => method.clone(), 102 "path" => path.clone(), 103 "status" => status.clone() ··· 105 .increment(1); 106 107 histogram!( 108 + "tranquil_pds_http_request_duration_seconds", 109 "method" => method, 110 "path" => path 111 ) ··· 135 } 136 137 pub fn record_auth_cache_hit(cache_type: &str) { 138 + counter!("tranquil_pds_auth_cache_hits_total", "cache_type" => cache_type.to_string()).increment(1); 139 } 140 141 pub fn record_auth_cache_miss(cache_type: &str) { 142 + counter!("tranquil_pds_auth_cache_misses_total", "cache_type" => cache_type.to_string()).increment(1); 143 } 144 145 pub fn set_firehose_subscribers(count: usize) { 146 + gauge!("tranquil_pds_firehose_subscribers").set(count as f64); 147 } 148 149 pub fn increment_firehose_subscribers() { 150 + counter!("tranquil_pds_firehose_events_total").increment(1); 151 } 152 153 pub fn record_firehose_event() { 154 + counter!("tranquil_pds_firehose_events_total").increment(1); 155 } 156 157 pub fn record_block_operation(op_type: &str) { 158 + counter!("tranquil_pds_block_operations_total", "op_type" => op_type.to_string()).increment(1); 159 } 160 161 pub fn record_s3_operation(op_type: &str, status: &str) { 162 counter!( 163 + "tranquil_pds_s3_operations_total", 164 "op_type" => op_type.to_string(), 165 "status" => status.to_string() 166 ) ··· 168 } 169 170 pub fn set_comms_queue_size(size: usize) { 171 + gauge!("tranquil_pds_comms_queue_size").set(size as f64); 172 } 173 174 pub fn record_rate_limit_rejection(limiter: &str) { 175 + counter!("tranquil_pds_rate_limit_rejections_total", "limiter" => limiter.to_string()).increment(1); 176 } 177 178 pub fn record_db_query(query_type: &str, duration_seconds: f64) { 179 + counter!("tranquil_pds_db_queries_total", "query_type" => query_type.to_string()).increment(1); 180 histogram!( 181 + "tranquil_pds_db_query_duration_seconds", 182 "query_type" => query_type.to_string() 183 ) 184 .record(duration_seconds);
+11 -11
tests/account_notifications.rs
··· 1 mod common; 2 use common::{base_url, client, create_account_and_login, get_db_connection_string}; 3 - use bspds::comms::{NewComms, CommsType, enqueue_comms}; 4 use serde_json::{Value, json}; 5 use sqlx::PgPool; 6 ··· 37 } 38 39 let resp = client 40 - .get(format!("{}/xrpc/com.bspds.account.getNotificationHistory", base)) 41 .header("Authorization", format!("Bearer {}", token)) 42 .send() 43 .await ··· 63 "discordId": "123456789" 64 }); 65 let resp = client 66 - .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base)) 67 .header("Authorization", format!("Bearer {}", token)) 68 .json(&prefs) 69 .send() ··· 92 "code": code 93 }); 94 let resp = client 95 - .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base)) 96 .header("Authorization", format!("Bearer {}", token)) 97 .json(&input) 98 .send() ··· 101 assert_eq!(resp.status(), 200); 102 103 let resp = client 104 - .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base)) 105 .header("Authorization", format!("Bearer {}", token)) 106 .send() 107 .await ··· 121 "telegramUsername": "testuser" 122 }); 123 let resp = client 124 - .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base)) 125 .header("Authorization", format!("Bearer {}", token)) 126 .json(&prefs) 127 .send() ··· 134 "code": "000000" 135 }); 136 let resp = client 137 - .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base)) 138 .header("Authorization", format!("Bearer {}", token)) 139 .json(&input) 140 .send() ··· 154 "code": "123456" 155 }); 156 let resp = client 157 - .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base)) 158 .header("Authorization", format!("Bearer {}", token)) 159 .json(&input) 160 .send() ··· 175 "email": unique_email 176 }); 177 let resp = client 178 - .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base)) 179 .header("Authorization", format!("Bearer {}", token)) 180 .json(&prefs) 181 .send() ··· 203 "code": code 204 }); 205 let resp = client 206 - .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base)) 207 .header("Authorization", format!("Bearer {}", token)) 208 .json(&input) 209 .send() ··· 212 assert_eq!(resp.status(), 200); 213 214 let resp = client 215 - .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base)) 216 .header("Authorization", format!("Bearer {}", token)) 217 .send() 218 .await
··· 1 mod common; 2 use common::{base_url, client, create_account_and_login, get_db_connection_string}; 3 + use tranquil_pds::comms::{NewComms, CommsType, enqueue_comms}; 4 use serde_json::{Value, json}; 5 use sqlx::PgPool; 6 ··· 37 } 38 39 let resp = client 40 + .get(format!("{}/xrpc/com.tranquil.account.getNotificationHistory", base)) 41 .header("Authorization", format!("Bearer {}", token)) 42 .send() 43 .await ··· 63 "discordId": "123456789" 64 }); 65 let resp = client 66 + .post(format!("{}/xrpc/com.tranquil.account.updateNotificationPrefs", base)) 67 .header("Authorization", format!("Bearer {}", token)) 68 .json(&prefs) 69 .send() ··· 92 "code": code 93 }); 94 let resp = client 95 + .post(format!("{}/xrpc/com.tranquil.account.confirmChannelVerification", base)) 96 .header("Authorization", format!("Bearer {}", token)) 97 .json(&input) 98 .send() ··· 101 assert_eq!(resp.status(), 200); 102 103 let resp = client 104 + .get(format!("{}/xrpc/com.tranquil.account.getNotificationPrefs", base)) 105 .header("Authorization", format!("Bearer {}", token)) 106 .send() 107 .await ··· 121 "telegramUsername": "testuser" 122 }); 123 let resp = client 124 + .post(format!("{}/xrpc/com.tranquil.account.updateNotificationPrefs", base)) 125 .header("Authorization", format!("Bearer {}", token)) 126 .json(&prefs) 127 .send() ··· 134 "code": "000000" 135 }); 136 let resp = client 137 + .post(format!("{}/xrpc/com.tranquil.account.confirmChannelVerification", base)) 138 .header("Authorization", format!("Bearer {}", token)) 139 .json(&input) 140 .send() ··· 154 "code": "123456" 155 }); 156 let resp = client 157 + .post(format!("{}/xrpc/com.tranquil.account.confirmChannelVerification", base)) 158 .header("Authorization", format!("Bearer {}", token)) 159 .json(&input) 160 .send() ··· 175 "email": unique_email 176 }); 177 let resp = client 178 + .post(format!("{}/xrpc/com.tranquil.account.updateNotificationPrefs", base)) 179 .header("Authorization", format!("Bearer {}", token)) 180 .json(&prefs) 181 .send() ··· 203 "code": code 204 }); 205 let resp = client 206 + .post(format!("{}/xrpc/com.tranquil.account.confirmChannelVerification", base)) 207 .header("Authorization", format!("Bearer {}", token)) 208 .json(&input) 209 .send() ··· 212 assert_eq!(resp.status(), 200); 213 214 let resp = client 215 + .get(format!("{}/xrpc/com.tranquil.account.getNotificationPrefs", base)) 216 .header("Authorization", format!("Bearer {}", token)) 217 .send() 218 .await
+2 -2
tests/admin_stats.rs
··· 11 let (_, _) = create_admin_account_and_login(&client).await; 12 13 let resp = client 14 - .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base)) 15 .header("Authorization", format!("Bearer {}", token1)) 16 .send() 17 .await ··· 33 let client = client(); 34 let base = base_url().await; 35 let resp = client 36 - .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base)) 37 .send() 38 .await 39 .unwrap();
··· 11 let (_, _) = create_admin_account_and_login(&client).await; 12 13 let resp = client 14 + .get(format!("{}/xrpc/com.tranquil.admin.getServerStats", base)) 15 .header("Authorization", format!("Bearer {}", token1)) 16 .send() 17 .await ··· 33 let client = client(); 34 let base = base_url().await; 35 let resp = client 36 + .get(format!("{}/xrpc/com.tranquil.admin.getServerStats", base)) 37 .send() 38 .await 39 .unwrap();
+6 -6
tests/change_password.rs
··· 33 let jwt = verify_new_account(&client, did).await; 34 let change_res = client 35 .post(format!( 36 - "{}/xrpc/com.bspds.account.changePassword", 37 base_url().await 38 )) 39 .bearer_auth(&jwt) ··· 79 let (_, jwt) = setup_new_user("change-pw-wrong").await; 80 let res = client 81 .post(format!( 82 - "{}/xrpc/com.bspds.account.changePassword", 83 base_url().await 84 )) 85 .bearer_auth(&jwt) ··· 122 let jwt = verify_new_account(&client, did).await; 123 let res = client 124 .post(format!( 125 - "{}/xrpc/com.bspds.account.changePassword", 126 base_url().await 127 )) 128 .bearer_auth(&jwt) ··· 144 let (_, jwt) = setup_new_user("change-pw-empty").await; 145 let res = client 146 .post(format!( 147 - "{}/xrpc/com.bspds.account.changePassword", 148 base_url().await 149 )) 150 .bearer_auth(&jwt) ··· 164 let (_, jwt) = setup_new_user("change-pw-emptynew").await; 165 let res = client 166 .post(format!( 167 - "{}/xrpc/com.bspds.account.changePassword", 168 base_url().await 169 )) 170 .bearer_auth(&jwt) ··· 183 let client = client(); 184 let res = client 185 .post(format!( 186 - "{}/xrpc/com.bspds.account.changePassword", 187 base_url().await 188 )) 189 .json(&json!({
··· 33 let jwt = verify_new_account(&client, did).await; 34 let change_res = client 35 .post(format!( 36 + "{}/xrpc/com.tranquil.account.changePassword", 37 base_url().await 38 )) 39 .bearer_auth(&jwt) ··· 79 let (_, jwt) = setup_new_user("change-pw-wrong").await; 80 let res = client 81 .post(format!( 82 + "{}/xrpc/com.tranquil.account.changePassword", 83 base_url().await 84 )) 85 .bearer_auth(&jwt) ··· 122 let jwt = verify_new_account(&client, did).await; 123 let res = client 124 .post(format!( 125 + "{}/xrpc/com.tranquil.account.changePassword", 126 base_url().await 127 )) 128 .bearer_auth(&jwt) ··· 144 let (_, jwt) = setup_new_user("change-pw-empty").await; 145 let res = client 146 .post(format!( 147 + "{}/xrpc/com.tranquil.account.changePassword", 148 base_url().await 149 )) 150 .bearer_auth(&jwt) ··· 164 let (_, jwt) = setup_new_user("change-pw-emptynew").await; 165 let res = client 166 .post(format!( 167 + "{}/xrpc/com.tranquil.account.changePassword", 168 base_url().await 169 )) 170 .bearer_auth(&jwt) ··· 183 let client = client(); 184 let res = client 185 .post(format!( 186 + "{}/xrpc/com.tranquil.account.changePassword", 187 base_url().await 188 )) 189 .json(&json!({
+10 -10
tests/common/mod.rs
··· 1 use aws_config::BehaviorVersion; 2 use aws_sdk_s3::Client as S3Client; 3 use aws_sdk_s3::config::Credentials; 4 - use bspds::state::AppState; 5 use chrono::Utc; 6 use reqwest::{Client, StatusCode, header}; 7 use serde_json::{Value, json}; ··· 40 pub const TARGET_DID: &str = "did:plc:target"; 41 42 fn has_external_infra() -> bool { 43 - std::env::var("BSPDS_TEST_INFRA_READY").is_ok() 44 || (std::env::var("DATABASE_URL").is_ok() && std::env::var("S3_ENDPOINT").is_ok()) 45 } 46 #[cfg(test)] ··· 51 } 52 if std::env::var("XDG_RUNTIME_DIR").is_ok() { 53 let _ = std::process::Command::new("podman") 54 - .args(&["rm", "-f", "--filter", "label=bspds_test=true"]) 55 .output(); 56 } 57 let _ = std::process::Command::new("docker") ··· 60 "prune", 61 "-f", 62 "--filter", 63 - "label=bspds_test=true", 64 ]) 65 .output(); 66 } ··· 80 let (tx, rx) = std::sync::mpsc::channel(); 81 std::thread::spawn(move || { 82 unsafe { 83 - std::env::set_var("BSPDS_ALLOW_INSECURE_SECRETS", "1"); 84 } 85 if std::env::var("DOCKER_HOST").is_err() { 86 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { ··· 152 .with_env_var("MINIO_ROOT_USER", "minioadmin") 153 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") 154 .with_cmd(vec!["server".to_string(), "/data".to_string()]) 155 - .with_label("bspds_test", "true") 156 .start() 157 .await 158 .expect("Failed to start MinIO"); ··· 195 S3_CONTAINER.set(s3_container).ok(); 196 let container = Postgres::default() 197 .with_tag("18-alpine") 198 - .with_label("bspds_test", "true") 199 .start() 200 .await 201 .expect("Failed to start Postgres"); ··· 236 } 237 238 async fn spawn_app(database_url: String) -> String { 239 - use bspds::rate_limit::RateLimiters; 240 let pool = PgPoolOptions::new() 241 .max_connections(50) 242 .connect(&database_url) ··· 260 .with_oauth_authorize_limit(10000) 261 .with_oauth_token_limit(10000); 262 let state = AppState::new(pool).await.with_rate_limiters(rate_limiters); 263 - bspds::sync::listener::start_sequencer_listener(state.clone()).await; 264 - let app = bspds::app(state); 265 tokio::spawn(async move { 266 axum::serve(listener, app).await.unwrap(); 267 });
··· 1 use aws_config::BehaviorVersion; 2 use aws_sdk_s3::Client as S3Client; 3 use aws_sdk_s3::config::Credentials; 4 + use tranquil_pds::state::AppState; 5 use chrono::Utc; 6 use reqwest::{Client, StatusCode, header}; 7 use serde_json::{Value, json}; ··· 40 pub const TARGET_DID: &str = "did:plc:target"; 41 42 fn has_external_infra() -> bool { 43 + std::env::var("TRANQUIL_PDS_TEST_INFRA_READY").is_ok() 44 || (std::env::var("DATABASE_URL").is_ok() && std::env::var("S3_ENDPOINT").is_ok()) 45 } 46 #[cfg(test)] ··· 51 } 52 if std::env::var("XDG_RUNTIME_DIR").is_ok() { 53 let _ = std::process::Command::new("podman") 54 + .args(&["rm", "-f", "--filter", "label=tranquil_pds_test=true"]) 55 .output(); 56 } 57 let _ = std::process::Command::new("docker") ··· 60 "prune", 61 "-f", 62 "--filter", 63 + "label=tranquil_pds_test=true", 64 ]) 65 .output(); 66 } ··· 80 let (tx, rx) = std::sync::mpsc::channel(); 81 std::thread::spawn(move || { 82 unsafe { 83 + std::env::set_var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS", "1"); 84 } 85 if std::env::var("DOCKER_HOST").is_err() { 86 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { ··· 152 .with_env_var("MINIO_ROOT_USER", "minioadmin") 153 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") 154 .with_cmd(vec!["server".to_string(), "/data".to_string()]) 155 + .with_label("tranquil_pds_test", "true") 156 .start() 157 .await 158 .expect("Failed to start MinIO"); ··· 195 S3_CONTAINER.set(s3_container).ok(); 196 let container = Postgres::default() 197 .with_tag("18-alpine") 198 + .with_label("tranquil_pds_test", "true") 199 .start() 200 .await 201 .expect("Failed to start Postgres"); ··· 236 } 237 238 async fn spawn_app(database_url: String) -> String { 239 + use tranquil_pds::rate_limit::RateLimiters; 240 let pool = PgPoolOptions::new() 241 .max_connections(50) 242 .connect(&database_url) ··· 260 .with_oauth_authorize_limit(10000) 261 .with_oauth_token_limit(10000); 262 let state = AppState::new(pool).await.with_rate_limiters(rate_limiters); 263 + tranquil_pds::sync::listener::start_sequencer_listener(state.clone()).await; 264 + let app = tranquil_pds::app(state); 265 tokio::spawn(async move { 266 axum::serve(listener, app).await.unwrap(); 267 });
+1 -1
tests/image_processing.rs
··· 1 - use bspds::image::{ 2 DEFAULT_MAX_FILE_SIZE, ImageError, ImageProcessor, OutputFormat, THUMB_SIZE_FEED, 3 THUMB_SIZE_FULL, 4 };
··· 1 + use tranquil_pds::image::{ 2 DEFAULT_MAX_FILE_SIZE, ImageError, ImageProcessor, OutputFormat, THUMB_SIZE_FEED, 3 THUMB_SIZE_FULL, 4 };
+1 -1
tests/import_with_verification.rs
··· 194 .fetch_optional(&pool) 195 .await 196 .ok()??; 197 - bspds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 198 } 199 #[tokio::test] 200 #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_valid_signature_and_mock_plc -- --ignored --test-threads=1"]
··· 194 .fetch_optional(&pool) 195 .await 196 .ok()??; 197 + tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 198 } 199 #[tokio::test] 200 #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_valid_signature_and_mock_plc -- --ignored --test-threads=1"]
+20 -4
tests/jwt_security.rs
··· 1 #![allow(unused_imports)] 2 mod common; 3 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4 - use bspds::auth::{ 5 self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, 6 TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token, 7 create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token, ··· 409 } 410 411 #[tokio::test] 412 - async fn test_deactivated_account_rejected() { 413 let url = base_url().await; 414 let http_client = client(); 415 let (access_jwt, _did) = create_account_and_login(&http_client).await; ··· 423 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url)) 424 .header("Authorization", format!("Bearer {}", access_jwt)) 425 .send().await.unwrap(); 426 - assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 427 let body: Value = res.json().await.unwrap(); 428 - assert_eq!(body["error"], "AccountDeactivated"); 429 } 430 431 #[tokio::test]
··· 1 #![allow(unused_imports)] 2 mod common; 3 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4 + use tranquil_pds::auth::{ 5 self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, 6 TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token, 7 create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token, ··· 409 } 410 411 #[tokio::test] 412 + async fn test_deactivated_account_behavior() { 413 let url = base_url().await; 414 let http_client = client(); 415 let (access_jwt, _did) = create_account_and_login(&http_client).await; ··· 423 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url)) 424 .header("Authorization", format!("Bearer {}", access_jwt)) 425 .send().await.unwrap(); 426 + assert_eq!(res.status(), StatusCode::OK); 427 let body: Value = res.json().await.unwrap(); 428 + assert_eq!(body["active"], false); 429 + 430 + let post_res = http_client.post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 431 + .header("Authorization", format!("Bearer {}", access_jwt)) 432 + .json(&json!({ 433 + "repo": _did, 434 + "collection": "app.bsky.feed.post", 435 + "record": { 436 + "$type": "app.bsky.feed.post", 437 + "text": "test", 438 + "createdAt": "2024-01-01T00:00:00Z" 439 + } 440 + })) 441 + .send().await.unwrap(); 442 + assert_eq!(post_res.status(), StatusCode::UNAUTHORIZED); 443 + let post_body: Value = post_res.json().await.unwrap(); 444 + assert_eq!(post_body["error"], "AccountDeactivated"); 445 } 446 447 #[tokio::test]
+1 -1
tests/notifications.rs
··· 1 mod common; 2 - use bspds::comms::{ 3 CommsChannel, CommsStatus, CommsType, NewComms, enqueue_comms, enqueue_welcome, 4 }; 5 use sqlx::PgPool;
··· 1 mod common; 2 + use tranquil_pds::comms::{ 3 CommsChannel, CommsStatus, CommsType, NewComms, enqueue_comms, enqueue_welcome, 4 }; 5 use sqlx::PgPool;
+1 -1
tests/oauth_security.rs
··· 2 mod common; 3 mod helpers; 4 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5 - use bspds::oauth::dpop::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint}; 6 use chrono::Utc; 7 use common::{base_url, client}; 8 use helpers::verify_new_account;
··· 2 mod common; 3 mod helpers; 4 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5 + use tranquil_pds::oauth::dpop::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint}; 6 use chrono::Utc; 7 use common::{base_url, client}; 8 use helpers::verify_new_account;
+1 -1
tests/plc_migration.rs
··· 50 .fetch_optional(&pool) 51 .await 52 .ok()??; 53 - bspds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 54 } 55 56 async fn get_plc_token_from_db(did: &str) -> Option<String> {
··· 50 .fetch_optional(&pool) 51 .await 52 .ok()??; 53 + tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 54 } 55 56 async fn get_plc_token_from_db(did: &str) -> Option<String> {
+1 -1
tests/plc_validation.rs
··· 1 - use bspds::plc::{ 2 PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation, 3 signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission, 4 verify_operation_signature,
··· 1 + use tranquil_pds::plc::{ 2 PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation, 3 signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission, 4 verify_operation_signature,
+1 -1
tests/rate_limit.rs
··· 162 println!("VALKEY_URL not set, skipping distributed rate limiter test"); 163 return; 164 } 165 - use bspds::cache::{DistributedRateLimiter, RedisRateLimiter}; 166 let valkey_url = std::env::var("VALKEY_URL").unwrap(); 167 let client = redis::Client::open(valkey_url.as_str()).expect("Failed to create Redis client"); 168 let conn = client
··· 162 println!("VALKEY_URL not set, skipping distributed rate limiter test"); 163 return; 164 } 165 + use tranquil_pds::cache::{DistributedRateLimiter, RedisRateLimiter}; 166 let valkey_url = std::env::var("VALKEY_URL").unwrap(); 167 let client = redis::Client::open(valkey_url.as_str()).expect("Failed to create Redis client"); 168 let conn = client
+1 -1
tests/record_validation.rs
··· 1 - use bspds::validation::{ 2 RecordValidator, ValidationError, ValidationStatus, validate_collection_nsid, 3 validate_record_key, 4 };
··· 1 + use tranquil_pds::validation::{ 2 RecordValidator, ValidationError, ValidationStatus, validate_collection_nsid, 3 validate_record_key, 4 };
+3 -3
tests/security_fixes.rs
··· 1 mod common; 2 - use bspds::image::{ImageError, ImageProcessor}; 3 - use bspds::comms::{SendError, is_valid_phone_number, sanitize_header_value}; 4 - use bspds::oauth::templates::{error_page, login_page, success_page}; 5 6 #[test] 7 fn test_header_injection_sanitization() {
··· 1 mod common; 2 + use tranquil_pds::image::{ImageError, ImageProcessor}; 3 + use tranquil_pds::comms::{SendError, is_valid_phone_number, sanitize_header_value}; 4 + use tranquil_pds::oauth::templates::{error_page, login_page, success_page}; 5 6 #[test] 7 fn test_header_injection_sanitization() {
+9 -9
tests/session_management.rs
··· 11 let (did, jwt) = setup_new_user("list-sessions").await; 12 let res = client 13 .get(format!( 14 - "{}/xrpc/com.bspds.account.listSessions", 15 base_url().await 16 )) 17 .bearer_auth(&jwt) ··· 74 let jwt2 = login_body["accessJwt"].as_str().unwrap(); 75 let list_res = client 76 .get(format!( 77 - "{}/xrpc/com.bspds.account.listSessions", 78 base_url().await 79 )) 80 .bearer_auth(jwt2) ··· 93 let client = client(); 94 let res = client 95 .get(format!( 96 - "{}/xrpc/com.bspds.account.listSessions", 97 base_url().await 98 )) 99 .send() ··· 145 let jwt2 = login_body["accessJwt"].as_str().unwrap(); 146 let list_res = client 147 .get(format!( 148 - "{}/xrpc/com.bspds.account.listSessions", 149 base_url().await 150 )) 151 .bearer_auth(jwt2) ··· 159 let session_id = other_session.unwrap()["id"].as_str().unwrap(); 160 let revoke_res = client 161 .post(format!( 162 - "{}/xrpc/com.bspds.account.revokeSession", 163 base_url().await 164 )) 165 .bearer_auth(jwt2) ··· 170 assert_eq!(revoke_res.status(), StatusCode::OK); 171 let list_after_res = client 172 .get(format!( 173 - "{}/xrpc/com.bspds.account.listSessions", 174 base_url().await 175 )) 176 .bearer_auth(jwt2) ··· 190 let (_, jwt) = setup_new_user("revoke-invalid").await; 191 let res = client 192 .post(format!( 193 - "{}/xrpc/com.bspds.account.revokeSession", 194 base_url().await 195 )) 196 .bearer_auth(&jwt) ··· 207 let (_, jwt) = setup_new_user("revoke-notfound").await; 208 let res = client 209 .post(format!( 210 - "{}/xrpc/com.bspds.account.revokeSession", 211 base_url().await 212 )) 213 .bearer_auth(&jwt) ··· 223 let client = client(); 224 let res = client 225 .post(format!( 226 - "{}/xrpc/com.bspds.account.revokeSession", 227 base_url().await 228 )) 229 .json(&json!({"sessionId": "1"}))
··· 11 let (did, jwt) = setup_new_user("list-sessions").await; 12 let res = client 13 .get(format!( 14 + "{}/xrpc/com.tranquil.account.listSessions", 15 base_url().await 16 )) 17 .bearer_auth(&jwt) ··· 74 let jwt2 = login_body["accessJwt"].as_str().unwrap(); 75 let list_res = client 76 .get(format!( 77 + "{}/xrpc/com.tranquil.account.listSessions", 78 base_url().await 79 )) 80 .bearer_auth(jwt2) ··· 93 let client = client(); 94 let res = client 95 .get(format!( 96 + "{}/xrpc/com.tranquil.account.listSessions", 97 base_url().await 98 )) 99 .send() ··· 145 let jwt2 = login_body["accessJwt"].as_str().unwrap(); 146 let list_res = client 147 .get(format!( 148 + "{}/xrpc/com.tranquil.account.listSessions", 149 base_url().await 150 )) 151 .bearer_auth(jwt2) ··· 159 let session_id = other_session.unwrap()["id"].as_str().unwrap(); 160 let revoke_res = client 161 .post(format!( 162 + "{}/xrpc/com.tranquil.account.revokeSession", 163 base_url().await 164 )) 165 .bearer_auth(jwt2) ··· 170 assert_eq!(revoke_res.status(), StatusCode::OK); 171 let list_after_res = client 172 .get(format!( 173 + "{}/xrpc/com.tranquil.account.listSessions", 174 base_url().await 175 )) 176 .bearer_auth(jwt2) ··· 190 let (_, jwt) = setup_new_user("revoke-invalid").await; 191 let res = client 192 .post(format!( 193 + "{}/xrpc/com.tranquil.account.revokeSession", 194 base_url().await 195 )) 196 .bearer_auth(&jwt) ··· 207 let (_, jwt) = setup_new_user("revoke-notfound").await; 208 let res = client 209 .post(format!( 210 + "{}/xrpc/com.tranquil.account.revokeSession", 211 base_url().await 212 )) 213 .bearer_auth(&jwt) ··· 223 let client = client(); 224 let res = client 225 .post(format!( 226 + "{}/xrpc/com.tranquil.account.revokeSession", 227 base_url().await 228 )) 229 .json(&json!({"sessionId": "1"}))