authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-05-13 01:51:04-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-05-14 21:43:45-07:00
logf9eb79553ff10e71e7957c722bd47c7e82ccc543
tree484d2bdcffeef3a0bb17d8b41dbe25ba83919e49
parent7660b1882cbb0f3a9d41fae2ec3fe1c4182f568d
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

feat: atproto stuff


9 files changed, 352 insertions(+), 3 deletions(-)

compose.yaml+147
...@@ -215,6 +215,76 @@ services:...@@ -215,6 +215,76 @@ services:
215 - "${APP_ROOT}/forgejo-runner:/data"215 - "${APP_ROOT}/forgejo-runner:/data"
216 restart: unless-stopped216 restart: unless-stopped
217 command: forgejo-runner daemon217 command: forgejo-runner daemon
218 knot: # port 22
219 container_name: knot
220 image: knot:latest
221 build:
222 context: https://tangled.org/tangled.org/3lqs6zdi4nt22.git
223 dockerfile: Dockerfile
224 args:
225 TAG: "${KNOT_IMAGE_TAG:-v1.14.0-alpha}"
226 UID: "${USER_ID:?}"
227 GID: "${GROUP_ID:?}"
228 pull_policy: build
229 depends_on:
230 knot-init:
231 condition: service_completed_successfully
232 environment:
233 KNOT_SERVER_HOSTNAME: "knot.${HOME_DOMAIN:?}"
234 KNOT_SERVER_OWNER: "${KNOT_SERVER_OWNER:?}"
235 KNOT_SERVER_DB_PATH: /app/knotserver.db
236 KNOT_REPO_SCAN_PATH: /home/git/repositories
237 KNOT_SERVER_INTERNAL_LISTEN_ADDR: localhost:5444
238 APPVIEW_ENDPOINT: "${KNOT_APPVIEW_ENDPOINT:-https://tangled.org}"
239 ports:
240 - "${KNOT_SSH_PORT:-2222}:22"
241 restart: unless-stopped
242 volumes:
243 - "${APP_ROOT}/knot/keys:/etc/ssh/keys"
244 - "${APP_ROOT}/knot/repositories:/home/git/repositories"
245 - "${APP_ROOT}/knot/server:/app"
246 labels:
247 net.paperclover.list.name: Tangled Knot
248 net.paperclover.list.domain: knot
249 net.paperclover.list.priority: 106
250 knot-init:
251 container_name: knot-init
252 image: alpine:3.22
253 user: "0:0"
254 command: ["sh", "-c", "chown -R ${USER_ID:?}:${GROUP_ID:?} /knot"]
255 volumes:
256 - "${APP_ROOT}/knot:/knot"
257 spindle: # port 6555
258 container_name: spindle
259 build:
260 context: ./config/spindle
261 dockerfile: Dockerfile
262 args:
263 TAG: "${SPINDLE_IMAGE_TAG:-v1.14.0-alpha}"
264 pull_policy: build
265 depends_on:
266 docker-in-docker:
267 condition: service_started
268 environment:
269 DOCKER_HOST: tcp://docker-in-docker:2375
270 SPINDLE_SERVER_HOSTNAME: "spindle.${HOME_DOMAIN:?}"
271 SPINDLE_SERVER_OWNER: "${SPINDLE_SERVER_OWNER:?}"
272 SPINDLE_SERVER_DB_PATH: /data/spindle.db
273 SPINDLE_SERVER_LOG_DIR: /data/logs
274 SPINDLE_SERVER_TAP_EMBED: "true"
275 SPINDLE_SERVER_TAP_URL: http://127.0.0.1:2480
276 SPINDLE_SERVER_TAP_BIND: 127.0.0.1:2480
277 SPINDLE_SERVER_TAP_DB_PATH: /data/tap.db
278 SPINDLE_NIXERY_PIPELINES_NIXERY: "${SPINDLE_NIXERY_PIPELINES_NIXERY:-nixery.tangled.sh}"
279 SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT: "${SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT:-10m}"
280 user: "$USER_ID:$GROUP_ID"
281 restart: unless-stopped
282 volumes:
283 - "${APP_ROOT}/spindle:/data"
284 labels:
285 net.paperclover.list.name: Tangled Spindle
286 net.paperclover.list.domain: spindle
287 net.paperclover.list.priority: 107
218 docker-in-docker:288 docker-in-docker:
219 container_name: docker-in-docker289 container_name: docker-in-docker
220 image: docker:dind290 image: docker:dind
...@@ -380,6 +450,43 @@ services:...@@ -380,6 +450,43 @@ services:
380 net.paperclover.list.domain: opencode450 net.paperclover.list.domain: opencode
381 net.paperclover.list.priority: 70451 net.paperclover.list.priority: 70
382 net.paperclover.list.access: personal452 net.paperclover.list.access: personal
453 # AT Protocol account hosting
454 pds: # port 3000
455 container_name: pds
456 image: ghcr.io/bluesky-social/pds:0.4
457 entrypoint: ["/bin/sh", "/custom/pds-entry.sh"]
458 user: "0:0"
459 restart: unless-stopped
460 environment:
461 HOME_DOMAIN: "${HOME_DOMAIN:?}"
462 PDS_HOSTNAME: "at.${HOME_DOMAIN:?}"
463 PDS_JWT_SECRET: "${PDS_JWT_SECRET:?}"
464 PDS_ADMIN_PASSWORD: "${PDS_ADMIN_PASSWORD:?}"
465 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: "${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX:?}"
466 PDS_DATA_DIRECTORY: /pds
467 PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks
468 PDS_DID_PLC_URL: https://plc.directory
469 PDS_BSKY_APP_VIEW_URL: https://api.bsky.app
470 PDS_BSKY_APP_VIEW_DID: did:web:api.bsky.app
471 PDS_REPORT_SERVICE_URL: https://mod.bsky.app
472 PDS_REPORT_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac
473 PDS_CRAWLERS: https://bsky.network
474 LOG_ENABLED: "true"
475 PDS_RATE_LIMITS_ENABLED: "${PDS_RATE_LIMITS_ENABLED:-true}"
476 PDS_INVITE_REQUIRED: "${PDS_INVITE_REQUIRED:-false}"
477 MAILER_ADDRESS: "${MAILER_ADDRESS}"
478 MAILER_USERNAME: "${MAILER_USERNAME}"
479 MAILER_PASSWORD: "${MAILER_PASSWORD}"
480 PDS_CONTACT_EMAIL_ADDRESS: "${PDS_CONTACT_EMAIL_ADDRESS:?}"
481 volumes:
482 - "${APP_ROOT}/pds:/pds"
483 - "./config/pds-entry.sh:/custom/pds-entry.sh:ro"
484 healthcheck:
485 test: ["CMD", "wget", "--quiet", "--spider", "http://127.0.0.1:3000/xrpc/_health"]
486 interval: 10s
487 timeout: 5s
488 retries: 12
489 start_period: 20s
383 # databases490 # databases
384 postgres:491 postgres:
385 container_name: postgres492 container_name: postgres
...@@ -552,6 +659,46 @@ services:...@@ -552,6 +659,46 @@ services:
552 net.paperclover.list.domain: sync659 net.paperclover.list.domain: sync
553 net.paperclover.list.priority: 31660 net.paperclover.list.priority: 31
554 net.paperclover.list.access: personal661 net.paperclover.list.access: personal
662 # chat
663 xmpp: # ports 5222, 5269
664 container_name: xmpp
665 build:
666 context: ./config/xmpp
667 dockerfile: Dockerfile
668 pull_policy: build
669 ports:
670 - 5222:5222
671 - 5269:5269
672 - 5000:5000
673 environment:
674 USER_ID: "$USER_ID"
675 GROUP_ID: "$GROUP_ID"
676 XMPP_HOSTNAME: "xmpp.${HOME_DOMAIN:?}"
677 XMPP_ADMIN_USER: "${XMPP_ADMIN_USER:?}"
678 XMPP_ADMIN_PASSWORD: "${XMPP_ADMIN_PASSWORD:?}"
679 ACME_EMAIL: "${ADMIN_EMAIL:?}"
680 PROSODY_VIRTUAL_HOSTS: "xmpp.${HOME_DOMAIN:?}"
681 PROSODY_NETWORK_HOSTNAME: "xmpp.${HOME_DOMAIN:?}"
682 PROSODY_ADMINS: "${XMPP_ADMIN_USER:?}@xmpp.${HOME_DOMAIN:?}"
683 PROSODY_COMPONENTS: "conference.xmpp.${HOME_DOMAIN:?}:muc,upload.xmpp.${HOME_DOMAIN:?}:http_file_share,proxy.xmpp.${HOME_DOMAIN:?}:proxy65"
684 PROSODY_MUC_MODULES: "muc_mam,muc_unique"
685 PROSODY_UPLOAD_LIMIT_MB: "100"
686 PROSODY_UPLOAD_STORAGE_GB: "10"
687 PROSODY_RETENTION_DAYS: "7"
688 PROSODY_LOGLEVEL: "info"
689 restart: unless-stopped
690 volumes:
691 - "${APP_ROOT}/xmpp/data:/var/lib/prosody"
692 - "${APP_ROOT}/xmpp/certs:/etc/prosody/certs"
693 - "${APP_ROOT}/xmpp/letsencrypt:/etc/letsencrypt"
694 - "./config/xmpp/conf.d:/etc/prosody/conf.d:ro"
695 labels:
696 net.paperclover.list.name: Paper Clover XMPP
697 net.paperclover.list.domain: xmpp
698 net.paperclover.list.priority: 108
699 depends_on:
700 caddy:
701 condition: service_started
555 # administration702 # administration
556 openspeedtest: # port 3000703 openspeedtest: # port 3000
557 restart: unless-stopped704 restart: unless-stopped
config/Caddyfile+44-2
...@@ -114,6 +114,15 @@ jkt.{$HOME_DOMAIN} {...@@ -114,6 +114,15 @@ jkt.{$HOME_DOMAIN} {
114 header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="114 header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
115 }115 }
116}116}
117at.{$HOME_DOMAIN} {
118 reverse_proxy "http://pds:3000"
119}
120knot.{$HOME_DOMAIN} {
121 reverse_proxy "http://knot:5555"
122}
123spindle.{$HOME_DOMAIN} {
124 reverse_proxy "http://spindle:6555"
125}
117music.{$HOME_DOMAIN} {126music.{$HOME_DOMAIN} {
118 handle_path /app/sw.js {127 handle_path /app/sw.js {
119 header Content-Type "application/javascript"128 header Content-Type "application/javascript"
...@@ -189,6 +198,41 @@ speedtest.{$HOME_DOMAIN} {...@@ -189,6 +198,41 @@ speedtest.{$HOME_DOMAIN} {
189sync.{$HOME_DOMAIN} {198sync.{$HOME_DOMAIN} {
190 import reverse_proxy_auth "http://syncthing:8384" admin199 import reverse_proxy_auth "http://syncthing:8384" admin
191}200}
201xmpp.{$HOME_DOMAIN} {
202 tls {
203 on_demand
204 }
205
206 handle /share* {
207 reverse_proxy "https://xmpp:5281" {
208 transport http {
209 tls_server_name xmpp.{$HOME_DOMAIN}
210 tls_insecure_skip_verify
211 }
212 }
213 }
214
215 handle /xmpp-websocket* {
216 reverse_proxy "https://xmpp:5281" {
217 transport http {
218 tls_server_name xmpp.{$HOME_DOMAIN}
219 tls_insecure_skip_verify
220 }
221 }
222 }
223
224 header Content-Type "text/plain; charset=utf-8"
225 respond "XMPP service. Use port 5222 for clients and port 5269 for federation." 200
226}
227
228http://xmpp.{$HOME_DOMAIN} {
229 handle /.well-known/acme-challenge/* {
230 reverse_proxy "http://xmpp:80"
231 }
232
233 header Content-Type "text/plain; charset=utf-8"
234 respond "XMPP service. Use port 5222 for clients and port 5269 for federation." 200
235}
192236
193# evil inc temporary infrastructure237# evil inc temporary infrastructure
194git.evil.inc {238git.evil.inc {
...@@ -241,5 +285,3 @@ nas.paperclover.net {...@@ -241,5 +285,3 @@ nas.paperclover.net {
241local.test, *.local.test {285local.test, *.local.test {
242 tls internal286 tls internal
243}287}
244
245
config/pds-entry.sh created+24
...@@ -0,0 +1,24 @@
1#!/bin/sh
2set -eu
3
4encode_uri_component() {
5 node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$1"
6}
7
8: "${MAILER_ADDRESS:?Missing MAILER_ADDRESS}"
9: "${MAILER_USERNAME:?Missing MAILER_USERNAME}"
10: "${MAILER_PASSWORD:?Missing MAILER_PASSWORD}"
11: "${HOME_DOMAIN:?Missing HOME_DOMAIN}"
12
13# PDS expects a single SMTP URL, so encode credentials before composing it.
14mailer_username=$(encode_uri_component "$MAILER_USERNAME")
15mailer_password=$(encode_uri_component "$MAILER_PASSWORD")
16export PDS_EMAIL_SMTP_URL="smtps://${mailer_username}:${mailer_password}@${MAILER_ADDRESS}:465/"
17export PDS_EMAIL_FROM_ADDRESS="noreply@${HOME_DOMAIN}"
18
19
20if [ "$#" -eq 0 ]; then
21 set -- node --enable-source-maps index.js
22fi
23
24exec dumb-init -- "$@"
config/spindle/Dockerfile created+16
...@@ -0,0 +1,16 @@
1FROM golang:1.25-alpine AS builder
2ENV CGO_ENABLED=1
3
4ARG TAG="v1.14.0-alpha"
5
6WORKDIR /app
7RUN apk add --no-cache git gcc musl-dev
8RUN git clone -b "${TAG}" https://tangled.org/@tangled.org/core .
9RUN go build -o /usr/local/bin/spindle -ldflags '-s -w -extldflags "-static"' ./cmd/spindle
10
11FROM alpine:latest
12RUN apk add --no-cache ca-certificates
13COPY --from=builder /usr/local/bin/spindle /usr/local/bin/spindle
14COPY entry.sh /entry.sh
15EXPOSE 6555
16ENTRYPOINT ["/bin/sh", "/entry.sh"]
config/spindle/entry.sh created+5
...@@ -0,0 +1,5 @@
1#!/bin/sh
2set -e
3
4mkdir -p "${SPINDLE_SERVER_LOG_DIR:-/data/logs}"
5exec /usr/local/bin/spindle
config/xmpp/Dockerfile created+11
...@@ -0,0 +1,11 @@
1FROM prosodyim/prosody:13.0
2
3RUN apt-get update \
4 && apt-get install -y --no-install-recommends certbot \
5 && rm -rf /var/lib/apt/lists/*
6
7COPY entry.sh /entry.sh
8RUN chmod 755 /entry.sh
9
10ENTRYPOINT ["/usr/bin/tini", "--", "/entry.sh"]
11CMD ["prosody", "-F"]
config/xmpp/conf.d/paperclover.cfg.lua created+16
...@@ -0,0 +1,16 @@
1allow_registration = true
2c2s_require_encryption = true
3http_external_url = "https://" .. Lua.os.getenv("XMPP_HOSTNAME") .. "/"
4contact_info = {
5 abuse = { "mailto:" .. Lua.os.getenv("ACME_EMAIL") };
6 admin = { "mailto:" .. Lua.os.getenv("ACME_EMAIL") };
7}
8
9modules_enabled:append({
10 "cloud_notify";
11 "smacks";
12 "csi";
13 "csi_simple";
14 "server_contact_info";
15 "websocket";
16})
config/xmpp/entry.sh created+60
...@@ -0,0 +1,60 @@
1#!/bin/bash
2set -euo pipefail
3
4if [[ "${1:-}" != "prosody" ]]; then
5 exec prosodyctl "$@"
6fi
7
8host_uid="${USER_ID:-}"
9host_gid="${GROUP_ID:-}"
10
11if [[ -n "$host_gid" ]]; then
12 groupmod -o -g "$host_gid" prosody
13fi
14
15if [[ -n "$host_uid" ]]; then
16 usermod -o -u "$host_uid" prosody
17fi
18
19mkdir -p /etc/prosody/certs /var/lib/prosody /var/run/prosody
20chown -R prosody:prosody /etc/prosody/certs /var/lib/prosody /var/run/prosody
21
22install_acme_cert() {
23 local host="$1"
24 local live_dir="/etc/letsencrypt/live/$host"
25
26 if [[ -f "$live_dir/fullchain.pem" ]]; then
27 certbot renew --non-interactive --quiet || true
28 else
29 certbot certonly \
30 --standalone \
31 --http-01-port 80 \
32 --preferred-challenges http \
33 --non-interactive \
34 --agree-tos \
35 --email "${ACME_EMAIL:?}" \
36 -d "$host"
37 fi
38
39 cp "$live_dir/fullchain.pem" "/etc/prosody/certs/$host.crt"
40 cp "$live_dir/privkey.pem" "/etc/prosody/certs/$host.key"
41 chown prosody:prosody "/etc/prosody/certs/$host.crt" "/etc/prosody/certs/$host.key"
42 chmod 640 "/etc/prosody/certs/$host.key"
43}
44
45register_account() {
46 local username="$1"
47 local password="$2"
48 local host="$3"
49
50 if [[ -z "$username" || -z "$password" ]]; then
51 return 0
52 fi
53
54 runuser -u prosody -- prosodyctl register "$username" "$host" "$password" >/dev/null 2>&1 || true
55}
56
57install_acme_cert "${XMPP_HOSTNAME:?}"
58register_account "${XMPP_ADMIN_USER:-}" "${XMPP_ADMIN_PASSWORD:-}" "${XMPP_HOSTNAME:?}"
59
60exec runuser -u prosody -- "$@"
generate-env.sh+29-1
...@@ -3,7 +3,7 @@ set -e...@@ -3,7 +3,7 @@ set -e
33
4FILE=${1:-.env}4FILE=${1:-.env}
5if [ -f "$FILE" ]; then5if [ -f "$FILE" ]; then
6 printf '\033[33mwarning: %d exists\033[0m\n' "$FILE"6 printf '\033[33mwarning: %s exists\033[0m\n' "$FILE"
7 exit 17 exit 1
8fi8fi
99
...@@ -28,6 +28,14 @@ template() {...@@ -28,6 +28,14 @@ template() {
28 add "MAILER_USERNAME" ""28 add "MAILER_USERNAME" ""
29 add "MAILER_PASSWORD" ""29 add "MAILER_PASSWORD" ""
3030
31 section "at protocol"
32 add "PDS_JWT_SECRET" "$(hex_secret 16)"
33 add "PDS_ADMIN_PASSWORD" "$(secret 32)"
34 add "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX" "$(pds_rotation_key)"
35 add "PDS_RATE_LIMITS_ENABLED" "true"
36 add "PDS_INVITE_REQUIRED" "false"
37 add "PDS_CONTACT_EMAIL_ADDRESS" ""
38
31 section "authentication"39 section "authentication"
32 add "KEYCLOAK_ADMIN_PASSWORD" "$(secret 12)"40 add "KEYCLOAK_ADMIN_PASSWORD" "$(secret 12)"
3341
...@@ -49,6 +57,19 @@ template() {...@@ -49,6 +57,19 @@ template() {
49 add "FORGEJO_SECURITY_SECRET_KEY" "$(forgejo generate secret SECRET_KEY)"57 add "FORGEJO_SECURITY_SECRET_KEY" "$(forgejo generate secret SECRET_KEY)"
50 add "FORGEJO_SECURITY_INTERNAL_TOKEN" "$(forgejo generate secret INTERNAL_TOKEN)"58 add "FORGEJO_SECURITY_INTERNAL_TOKEN" "$(forgejo generate secret INTERNAL_TOKEN)"
5159
60 section "tangled"
61 add "KNOT_SERVER_OWNER" ""
62 add "KNOT_APPVIEW_ENDPOINT" "https://tangled.org"
63 add "KNOT_SSH_PORT" "2222"
64 add "SPINDLE_SERVER_OWNER" ""
65 add "SPINDLE_IMAGE_TAG" "v1.14.0-alpha"
66 add "SPINDLE_NIXERY_PIPELINES_NIXERY" "nixery.tangled.sh"
67 add "SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT" "10m"
68
69 section "xmpp"
70 add "XMPP_ADMIN_USER" "admin"
71 add "XMPP_ADMIN_PASSWORD" "$(secret 20)"
72
52 section "evil infra"73 section "evil infra"
53 add "EVIL_FORGEJO_SERVER_LFS_JWT_SECRET" "$(forgejo generate secret LFS_JWT_SECRET)"74 add "EVIL_FORGEJO_SERVER_LFS_JWT_SECRET" "$(forgejo generate secret LFS_JWT_SECRET)"
54 add "EVIL_FORGEJO_OAUTH2_JWT_SECRET" "$(forgejo generate secret JWT_SECRET)"75 add "EVIL_FORGEJO_OAUTH2_JWT_SECRET" "$(forgejo generate secret JWT_SECRET)"
...@@ -57,6 +78,13 @@ template() {...@@ -57,6 +78,13 @@ template() {
57}78}
5879
59secret() { openssl rand -base64 "$1" | tr -d '\n=/' | cut -c1-"$1"; }80secret() { openssl rand -base64 "$1" | tr -d '\n=/' | cut -c1-"$1"; }
81hex_secret() { openssl rand -hex "$1"; }
82pds_rotation_key() {
83 openssl ecparam -name secp256k1 -genkey -noout -outform DER |
84 tail -c +8 |
85 head -c 32 |
86 xxd -p -c 32
87}
60section() { printf '\n# %s\n' "$1" >>"$FILE"; }88section() { printf '\n# %s\n' "$1" >>"$FILE"; }
61add() {89add() {
62 printf '%s=%s\n' "$1" "${2:-}" >>"$FILE"90 printf '%s=%s\n' "$1" "${2:-}" >>"$FILE"