+2
-2
.env.example
+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
+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
+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
-1
Cargo.toml
+2
-2
Dockerfile
+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
-1
README.md
+6
-6
deploy/quadlets/bspds-app.container
deploy/quadlets/tranquil-pds-app.container
+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
-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
+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
-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
+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
-1
deploy/quadlets/bspds.pod
deploy/quadlets/tranquil-pds.pod
+20
deploy/quadlets/tranquil-pds-db.container
+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
+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
+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
+1
-1
docker-compose.yaml
+42
-45
docs/install-alpine.md
+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
+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
+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
-1
docs/install-kubernetes.md
+36
-37
docs/install-openbsd.md
+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
+1
-1
frontend/index.html
+1
-1
frontend/package.json
+1
-1
frontend/package.json
+8
-8
frontend/src/lib/api.ts
+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
+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
+2
-2
frontend/src/lib/oauth.ts
+1
-1
frontend/src/routes/Register.svelte
+1
-1
frontend/src/routes/Register.svelte
+1
-1
frontend/src/routes/Verify.svelte
+1
-1
frontend/src/routes/Verify.svelte
+3
-3
frontend/src/tests/Dashboard.test.ts
+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
+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
+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
+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
+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
+1
-1
justfile
+4
-4
nginx.prod.conf
+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
+1
-1
observability/prometheus.yml
+75
-75
scripts/install-debian.sh
+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
+1
-1
scripts/run-tests.sh
+8
-8
scripts/test-infra.sh
+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
+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
+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, ¶ms.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, ¶ms.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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-1
tests/image_processing.rs
+1
-1
tests/import_with_verification.rs
+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
+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
-1
tests/notifications.rs
+1
-1
tests/oauth_security.rs
+1
-1
tests/oauth_security.rs
+1
-1
tests/plc_migration.rs
+1
-1
tests/plc_migration.rs
+1
-1
tests/plc_validation.rs
+1
-1
tests/plc_validation.rs
+1
-1
tests/rate_limit.rs
+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
-1
tests/record_validation.rs
+3
-3
tests/security_fixes.rs
+3
-3
tests/security_fixes.rs
···
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
+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"}))