From c2f17d5e005b9327b7699e1d3096a1b4581df835 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Thu, 07 May 2026 05:35:32 +0000 Subject: [PATCH] localinfra: sandboxed docker dev env sandboxing all microservices required to run Tangled including entire atproto infra in docker-compose. atproto infra: - did-method-plc (NOTE: linux/amd64 only) - pds - jetstream (NOTE: linux/amd64 only) tangled services: - knot - knotmirror - knotmirror-tap - appview (air hot reloaded) - tailwind-watch misc: - redis - postgres - caddy (reverse proxy) spindle is not included in this revision to make things simple. It needs some patches on appview mostly related to TLS handling in dev mode. Signed-off-by: Seongmin Lee --- .dockerignore | 38 ++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + docker-compose.yml | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ localinfra/Caddyfile | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ localinfra/appview.Dockerfile | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ localinfra/knot.Dockerfile | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ localinfra/knotmirror.Dockerfile | 26 ++++++++++++++++++++++++++ localinfra/pds.env | 13 +++++++++++++ localinfra/postgres-init.sql | 3 +++ localinfra/readme.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ localinfra/scripts/appview-static-files.sh | 44 ++++++++++++++++++++++++++++++++++++++++++++ localinfra/scripts/init-accounts.sh | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 12 file(s) changed, 846 insertion(s)(+), 0 deletion(s)(-) diff --git a/.dockerignore b/.dockerignore new file mode 100644 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +.git +.DS_Store +._.DS_Store +.direnv/ +.env* +*.qcow2 +genjwks.out +/nix/vm-data +blog/build +build/ +.wrangler/ +localinfra/certs/root.key + +appview/pages/static/* +!appview/pages/static/topbar-search.js + +sites/target +sites/.wrangler + +result +result-* +build +.bin +out/ +node_modules/ +*.db +*.db-* +*.rdb +*.bleve +**/*.bleve + +tmp +patches + +docker-compose.yml +docker-compose.*.yml +**/Dockerfile +**/*.Dockerfile diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ /nix/vm-data blog/build/ build/ .wrangler/ +localinfra/certs/* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,283 @@ +services: + redis: + image: redis:7-alpine + restart: unless-stopped + networks: [tngl] + + postgres: + image: postgres:14-alpine + restart: unless-stopped + environment: + POSTGRES_USER: tnglr + POSTGRES_PASSWORD: tnglr + volumes: + - postgres-data:/var/lib/postgresql/data + - ./localinfra/postgres-init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tnglr -d plc"] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s + networks: [tngl] + + pds: + image: ghcr.io/bluesky-social/pds:0.4.219 + restart: unless-stopped + env_file: localinfra/pds.env + environment: + PDS_DID_PLC_URL: http://plc:8080 + NODE_EXTRA_CA_CERTS: /caddy-ca/root.crt + volumes: + - pds-data:/pds + - ./localinfra/certs/root.crt:/caddy-ca/root.crt:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/xrpc/_health"] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s + depends_on: + plc: + condition: service_started + networks: [tngl] + + init-accounts: + image: alpine:3.22 + restart: "no" + env_file: localinfra/pds.env + environment: + PDS_URL: http://pds:3000 + OWNER_USER: alice + KNOT_HOSTNAME: knot.tngl.boltless.dev + volumes: + - ./localinfra/scripts/init-accounts.sh:/init.sh:ro + - init-state:/shared + command: sh -c "apk add --no-cache curl jq >/dev/null && sh /init.sh" + depends_on: + pds: + condition: service_healthy + networks: [tngl] + + plc: + platform: linux/amd64 + image: ghcr.io/bluesky-social/did-method-plc:plc-f2ab7516bac5bc0f3f86842fa94e996bd1b3815b + restart: unless-stopped + environment: + DEBUG_MODE: "1" + LOG_ENABLED: "true" + LOG_LEVEL: debug + LOG_DESTINATION: "1" + PLC_VERSION: 0.0.1 + PORT: "8080" + DB_CREDS_JSON: &DB_CREDS_JSON '{"username":"tnglr","password":"tnglr","host":"postgres","port":5432}' + DB_MIGRATE_CREDS_JSON: *DB_CREDS_JSON + depends_on: [postgres] + networks: [tngl] + + jetstream: + platform: linux/amd64 + image: ghcr.io/bluesky-social/jetstream:sha-d5a3b62 + restart: unless-stopped + environment: + JETSTREAM_DATA_DIR: /data + JETSTREAM_LIVENESS_TTL: 300s + JETSTREAM_WS_URL: wss://pds.tngl.boltless.dev/xrpc/com.atproto.sync.subscribeRepos + volumes: + - jetstream-data:/data + - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro + depends_on: + pds: + condition: service_healthy + networks: [tngl] + + knot: + build: + context: . + dockerfile: localinfra/knot.Dockerfile + restart: unless-stopped + environment: + KNOT_SERVER_HOSTNAME: knot.tngl.boltless.dev + KNOT_SERVER_LISTEN_ADDR: 0.0.0.0:5555 + KNOT_SERVER_INTERNAL_LISTEN_ADDR: 127.0.0.1:5444 + KNOT_SERVER_DB_PATH: /home/git/knotserver.db + KNOT_SERVER_PLC_URL: https://plc.tngl.boltless.dev + KNOT_SERVER_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe + KNOT_SERVER_DEV: "false" + KNOT_REPO_SCAN_PATH: /home/git/repositories + APPVIEW_ENDPOINT: https://tangled.org + KNOT_MIRRORS: https://mirror.tngl.boltless.dev + ports: + - "2222:22" + volumes: + - knot-data:/home/git + - knot-ssh-keys:/etc/ssh/keys + - init-state:/shared:ro + - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5555/"] + interval: 2s + timeout: 2s + retries: 60 + start_period: 30s + depends_on: + plc: + condition: service_started + jetstream: + condition: service_started + knotmirror: + condition: service_healthy + init-accounts: + condition: service_completed_successfully + networks: [tngl] + + knotmirror-tap: + image: ghcr.io/bluesky-social/indigo/tap:sha-4f47add43060c27e8a37d9d76482ecddf001fcd8 # 0.1.10 + restart: unless-stopped + environment: + TAP_BIND: ":2480" + TAP_PLC_URL: https://plc.tngl.boltless.dev + TAP_RELAY_URL: https://pds.tngl.boltless.dev # PDS can be used as basic relay without collectiondir. + TAP_DATABASE_URL: postgres://tnglr:tnglr@postgres:5432/mirror_tap?sslmode=disable + TAP_COLLECTION_FILTERS: sh.tangled.repo + TAP_SIGNAL_COLLECTION: sh.tangled.repo + TAP_RESYNC_PARALLELISM: "10" + TAP_RETRY_TIMEOUT: 60s + volumes: + - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro + depends_on: + postgres: + condition: service_started + pds: + condition: service_healthy + networks: [tngl] + + knotmirror: + build: + context: . + dockerfile: localinfra/knotmirror.Dockerfile + restart: unless-stopped + environment: + MIRROR_LISTEN: 0.0.0.0:7000 + MIRROR_ADMIN_LISTEN: 0.0.0.0:7200 + MIRROR_HOSTNAME: mirror.tngl.boltless.dev + MIRROR_TAP_URL: http://knotmirror-tap:2480 + MIRROR_DB_URL: postgres://tnglr:tnglr@postgres:5432/mirror?sslmode=disable + MIRROR_REDIS_ADDR: redis:6379 + MIRROR_PLC_URL: https://plc.tngl.boltless.dev + MIRROR_GIT_BASEPATH: /data/repos + MIRROR_KNOT_USE_SSL: "true" + MIRROR_KNOT_SSRF: "true" + MIRROR_RESYNC_PARALLELISM: "4" + volumes: + - knotmirror-data:/data + - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:7000/"] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s + ports: + - "7201:7200" + depends_on: + postgres: + condition: service_started + knotmirror-tap: + condition: service_started + networks: [tngl] + + tailwind: + image: d3fk/tailwindcss:v3 + restart: unless-stopped + working_dir: /build + init: true + environment: + BROWSERSLIST_IGNORE_OLD_DATA: "true" + volumes: + - ./tailwind.config.js:/build/tailwind.config.js:ro + - ./input.css:/build/input.css:ro + - ./appview/pages/templates:/build/appview/pages/templates:ro + - ./docs:/build/docs:ro + - ./blog/templates:/build/blog/templates:ro + - ./blog/posts:/build/blog/posts:ro + - ./appview/pages/static:/build/appview/pages/static + command: ["-i", "input.css", "-o", "appview/pages/static/tw.css", "--watch=always"] + network_mode: none + + appview: + build: + context: . + dockerfile: localinfra/appview.Dockerfile + restart: unless-stopped + environment: + TANGLED_DEV: "true" + TANGLED_APPVIEW_HOST: 127.0.0.1:3000 + TANGLED_DB_PATH: /var/lib/appview/appview.db + TANGLED_PLC_URL: https://plc.tngl.boltless.dev + TANGLED_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe + TANGLED_REDIS_ADDR: redis:6379 + TANGLED_KNOTMIRROR_URL: https://mirror.tngl.boltless.dev + ports: + - "3000:3000" + volumes: + - .:/src:cached + - go-cache:/go/cache + - go-mod-cache:/go/mod + - appview-data:/var/lib/appview + - init-state:/shared:ro + - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro + depends_on: + redis: + condition: service_started + pds: + condition: service_healthy + init-accounts: + condition: service_completed_successfully + networks: [tngl] + + caddy: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./localinfra/Caddyfile:/etc/caddy/Caddyfile + - ./localinfra/certs:/etc/caddy/certs:ro + - caddy-data:/data + networks: + tngl: + aliases: + - plc.tngl.boltless.dev + - pds.tngl.boltless.dev + - alice.pds.tngl.boltless.dev + - bob.pds.tngl.boltless.dev + - jetstream.tngl.boltless.dev + - knot.tngl.boltless.dev + - spindle.tngl.boltless.dev + - tngl.boltless.dev + - mirror.tngl.boltless.dev + +volumes: + caddy-data: + postgres-data: + pds-data: + jetstream-data: + knot-data: + knot-ssh-keys: + knotmirror-data: + init-state: + go-cache: + go-mod-cache: + appview-data: + +networks: + tngl: + driver: bridge + # Public-looking subnet so SSRF checks see container IPs as "public". + # RFC1918 + doc/benchmark ranges are blocklisted; 11.x is unrouted on + # the public internet, so it passes the check and won't collide. + ipam: + config: + - subnet: 11.0.0.0/24 diff --git a/localinfra/Caddyfile b/localinfra/Caddyfile new file mode 100644 --- /dev/null +++ b/localinfra/Caddyfile @@ -0,0 +1,50 @@ +{ + # use pinned CA from ./localinfra/certs + pki { + ca local { + name "Tangled Dev" + root_cn "Tangled Dev Root" + root { + format pem_file + cert /etc/caddy/certs/root.crt + key /etc/caddy/certs/root.key + } + } + } +} + +# did-method-plc +plc.tngl.boltless.dev { + tls internal + reverse_proxy plc:8080 +} + +# pds +*.pds.tngl.boltless.dev, pds.tngl.boltless.dev { + tls internal + reverse_proxy pds:3000 +} + +# jetstream +jetstream.tngl.boltless.dev { + tls internal + reverse_proxy jetstream:6008 +} + +# knot +knot.tngl.boltless.dev { + tls internal + reverse_proxy knot:5555 +} + +# knotmirror +mirror.tngl.boltless.dev { + tls internal + reverse_proxy knotmirror:7000 +} + +# appview +tngl.boltless.dev { + tls internal + reverse_proxy appview:3000 +} diff --git a/localinfra/appview.Dockerfile b/localinfra/appview.Dockerfile new file mode 100644 --- /dev/null +++ b/localinfra/appview.Dockerfile @@ -0,0 +1,53 @@ +# Development only. Not for production use. + +FROM golang:1.25-alpine + +RUN apk add --no-cache git build-base sqlite-dev tini sqlite-libs ca-certificates + +# air for live-reload +RUN go install github.com/air-verse/air@v1.65.1 && \ + mv /go/bin/air /usr/local/bin/air + +# goat for generating OAuth client key (moved out of indigo) +RUN go install github.com/bluesky-social/goat@v0.2.3 && \ + mv /go/bin/goat /usr/local/bin/goat + +ENV CGO_ENABLED=1 +ENV GOCACHE=/go/cache +ENV GOMODCACHE=/go/mod + +# Generates OAuth client key on first run. Persists to appview-data so re-runs +# reuse the same key. Mirrors flake.nix:221-222. +COPY <<'EOF' /usr/local/bin/appview-entrypoint.sh +#!/bin/sh +set -eu + +SECRET=/var/lib/appview/oauth-secret +KID=/var/lib/appview/oauth-kid + +if [ ! -s "$SECRET" ]; then + mkdir -p /var/lib/appview + goat key generate -t P-256 \ + | grep -A1 'Secret Key' | tail -n1 | awk '{print $1}' \ + > "$SECRET" + date +%s > "$KID" + echo "[oauth] generated kid=$(cat $KID)" +fi + +export TANGLED_OAUTH_CLIENT_SECRET="$(cat $SECRET)" +export TANGLED_OAUTH_CLIENT_KID="$(cat $KID)" + +# Pulled in from init-accounts via /shared (mounted ro). +[ -r /shared/label-defaults ] && export TANGLED_LABEL_DEFAULTS="$(cat /shared/label-defaults)" +[ -r /shared/label-gfi ] && export TANGLED_LABEL_GFI="$(cat /shared/label-gfi)" + +exec air -c /src/.air/appview.toml +EOF +RUN chmod +x /usr/local/bin/appview-entrypoint.sh + +WORKDIR /src + +EXPOSE 3000 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/usr/local/bin/appview-entrypoint.sh"] diff --git a/localinfra/knot.Dockerfile b/localinfra/knot.Dockerfile new file mode 100644 --- /dev/null +++ b/localinfra/knot.Dockerfile @@ -0,0 +1,96 @@ +# Development only. Not for production use. + +FROM golang:1.25-alpine AS builder + +RUN apk add --no-cache git build-base sqlite-dev + +ENV CGO_ENABLED=1 +ENV GOCACHE=/go/cache +ENV GOMODCACHE=/go/mod + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/cache \ + --mount=type=cache,target=/go/mod \ + go mod download + +COPY . . +RUN --mount=type=cache,target=/go/cache \ + --mount=type=cache,target=/go/mod \ + go build -tags libsqlite3 -o /out/knot ./cmd/knot + +FROM alpine:3.20 + +RUN apk add --no-cache git openssh-server tini sqlite-libs su-exec ca-certificates shadow openssl bash + +RUN groupadd -g 1000 -f git && \ + useradd -u 1000 -g 1000 -d /home/git -s /bin/sh -m git && \ + echo "git:$(openssl rand -hex 16)" | chpasswd + +COPY --from=builder /out/knot /usr/local/bin/knot +RUN chmod 0755 /usr/local/bin/knot + +COPY <<'EOF' /usr/local/bin/knot-keys-wrapper +#!/bin/sh +exec /usr/local/bin/knot keys -output authorized-keys \ + -internal-api "http://${KNOT_SERVER_INTERNAL_LISTEN_ADDR:-127.0.0.1:5444}" \ + -git-dir "${KNOT_REPO_SCAN_PATH:-/home/git/repositories}" \ + -log-path "/tmp/knotguard.log" +EOF +RUN chmod +x /usr/local/bin/knot-keys-wrapper + +# sshd config +COPY <<'EOF' /etc/ssh/sshd_config.d/knot.conf +PermitRootLogin no +PasswordAuthentication no +ChallengeResponseAuthentication no + +Match User git + AuthorizedKeysCommand /usr/local/bin/knot-keys-wrapper + AuthorizedKeysCommandUser nobody +EOF + +RUN echo 'Include /etc/ssh/sshd_config.d/*.conf' >> /etc/ssh/sshd_config + +COPY <<'EOF' /etc/ssh/sshd_config.d/host-keys.conf +HostKey /etc/ssh/keys/ssh_host_rsa_key +HostKey /etc/ssh/keys/ssh_host_ecdsa_key +HostKey /etc/ssh/keys/ssh_host_ed25519_key +EOF + +RUN mkdir -p /home/git/.config/git +COPY <<'EOF' /home/git/.config/git/config +[user] + name = Tangled + email = noreply@tangled.org +[receive] + advertisePushOptions = true +[uploadpack] + allowFilter = true + allowReachableSHA1InWant = true +EOF +RUN mkdir -p /home/git/repositories && chown -R git:git /home/git + +COPY <<'EOF' /usr/local/bin/knot-entrypoint.sh +#!/bin/sh +set -eu +[ -z "${KNOT_SERVER_OWNER:-}" ] && [ -r /shared/owner-did ] && \ + export KNOT_SERVER_OWNER="$(cat /shared/owner-did)" +: "${KNOT_SERVER_OWNER:?set via env or /shared/owner-did}" + +mkdir -p /etc/ssh/keys +[ -f /etc/ssh/keys/ssh_host_rsa_key ] || ssh-keygen -t rsa -f /etc/ssh/keys/ssh_host_rsa_key -q -N "" +[ -f /etc/ssh/keys/ssh_host_ecdsa_key ] || ssh-keygen -t ecdsa -f /etc/ssh/keys/ssh_host_ecdsa_key -q -N "" +[ -f /etc/ssh/keys/ssh_host_ed25519_key ] || ssh-keygen -t ed25519 -f /etc/ssh/keys/ssh_host_ed25519_key -q -N "" + +/usr/sbin/sshd -D -e & +exec su-exec git /usr/local/bin/knot server +EOF +RUN chmod +x /usr/local/bin/knot-entrypoint.sh + +VOLUME /home/git +EXPOSE 22 5555 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/usr/local/bin/knot-entrypoint.sh"] diff --git a/localinfra/knotmirror.Dockerfile b/localinfra/knotmirror.Dockerfile new file mode 100644 --- /dev/null +++ b/localinfra/knotmirror.Dockerfile @@ -0,0 +1,26 @@ +# Development only. Not for production use. + +FROM golang:1.25-alpine AS build + +RUN apk add --no-cache git + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /knotmirror ./cmd/knotmirror + +FROM alpine:3.22 + +RUN apk add --no-cache git tini ca-certificates + +# Trust dev CA in the system bundle so git/curl/openssl all accept caddy certs. +COPY localinfra/certs/root.crt /usr/local/share/ca-certificates/caddy.crt +RUN update-ca-certificates + +COPY --from=build /knotmirror /usr/local/bin/knotmirror + +EXPOSE 7000 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/usr/local/bin/knotmirror", "serve"] diff --git a/localinfra/pds.env b/localinfra/pds.env new file mode 100644 --- /dev/null +++ b/localinfra/pds.env @@ -0,0 +1,13 @@ +LOG_ENABLED=true + +PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a +PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3 +PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7 + +PDS_DATA_DIRECTORY=/pds +PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks + +PDS_DID_PLC_URL=http://localhost:8080 +PDS_HOSTNAME=pds.tngl.boltless.dev +PDS_PORT=3000 +# PDS_CRAWLERS=https://relay.tngl.boltless.dev diff --git a/localinfra/postgres-init.sql b/localinfra/postgres-init.sql new file mode 100644 --- /dev/null +++ b/localinfra/postgres-init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE plc; +CREATE DATABASE mirror; +CREATE DATABASE mirror_tap; diff --git a/localinfra/readme.md b/localinfra/readme.md new file mode 100644 --- /dev/null +++ b/localinfra/readme.md @@ -0,0 +1,48 @@ +Heavily inspired by [frontpage dev environment](https://github.com/frontpagefyi/frontpage/blob/10678df9c3f72cbd82f0856a9f99c74dd22326d8/apps/frontpage/local-infra/README.md). +Tangled's setup is slightly more involved because services inside the network need to reach the PDS over its **public** hostname with **valid TLS** — federation paths (DID resolution, OAuth, etc.) round-trip through the same URLs an external client would use. + +For example, resolving `alice.pds.tngl.boltless.dev` yields an `#atproto_pds` service pointing at `https://pds.tngl.boltless.dev`. Knot and spindle running inside docker must hit that exact URL and trust its cert. + +To make that work: + +- Caddy's dev root CA is mounted into every container that talks to another service over HTTPS. +- The Docker network uses an unrouted "public" subnet so the SSRF dialer doesn't reject container IPs as private. + +## What's inside: + +- [did-method-plc](https://github.com/did-method-plc/did-method-plc) () +- atproto_pds () +- jetstream () +- knot () +- knotmirror () +- appview () (live reloading) +- caddy reverse proxy + +> [!NOTE] +> Spindle is not included yet. + +## Setup + +1. Generate the dev CA from the repo root: + ```bash + mkdir -p localinfra/certs && + openssl req -x509 -newkey rsa:2048 \ + -keyout localinfra/certs/root.key \ + -out localinfra/certs/root.crt \ + -days 3650 -nodes \ + -subj "/CN=Tangled Dev CA" \ + -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \ + -addext "keyUsage=critical,keyCertSign,cRLSign" \ + -addext "nameConstraints=critical,permitted;DNS:tngl.boltless.dev" + ``` +2. Trust generated `localinfra/certs/root.crt` in your system's trust store. + - For example in MacOS, run + ```bash + sudo security add-trusted-cert -d -r trustRoot \ + -k /Library/Keychains/System.keychain \ + ./localinfra/certs/root.crt + ``` + - Depending on your browser you may have to import the certificate into your browser profiles too as some have their own certs do not use your system ones +3. run `./localinfra/scripts/appview-static-files.sh` +4. `docker compose up` +5. AppView will be running on `127.0.0.1:3000` with two test users: `alice.pds.tngl.boltless.dev` and `bob.pds.tngl.boltless.dev`. Both with password `password`. diff --git a/localinfra/scripts/appview-static-files.sh b/localinfra/scripts/appview-static-files.sh new file mode 100644 --- /dev/null +++ b/localinfra/scripts/appview-static-files.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +HTMX_URL="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" +HTMX_WS_URL="https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.2" +MERMAID_URL="https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js" +LUCIDE_URL="https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip" +INTER_URL="https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" +PLEX_MONO_URL="https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip" +ACTOR_TYPEAHEAD_REPO="https://tangled.org/@jakelazaroff.com/actor-typeahead" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUT="$REPO_ROOT/appview/pages/static" +TMP="$(mktemp -d)" +# trap 'rm -rf "$TMP"' EXIT + +mkdir -p "$OUT"/{fonts,icons,logos} + +curl -fsSL -o "$OUT/htmx.min.js" "$HTMX_URL" +curl -fsSL -o "$OUT/htmx-ext-ws.min.js" "$HTMX_WS_URL" +curl -fsSL -o "$OUT/mermaid.min.js" "$MERMAID_URL" + +curl -fsSL -o "$TMP/lucide.zip" "$LUCIDE_URL" +unzip -q "$TMP/lucide.zip" -d "$TMP/lucide" +cp -rf "$TMP"/lucide/icons/*.svg "$OUT/icons/" + +curl -fsSL -o "$TMP/inter.zip" "$INTER_URL" +unzip -q "$TMP/inter.zip" -d "$TMP/inter" +cp -f "$TMP"/inter/web/InterVariable*.woff2 "$OUT/fonts/" +cp -f "$TMP"/inter/web/InterDisplay*.woff2 "$OUT/fonts/" +cp -f "$TMP"/inter/InterVariable*.ttf "$OUT/fonts/" + +curl -fsSL -o "$TMP/plex.zip" "$PLEX_MONO_URL" +unzip -q "$TMP/plex.zip" -d "$TMP/plex" +cp -f "$TMP"/plex/ibm-plex-mono/fonts/complete/woff2/IBMPlexMono*.woff2 "$OUT/fonts/" + +git clone --depth=1 "$ACTOR_TYPEAHEAD_REPO" "$TMP/actor-typeahead" +cp -f "$TMP/actor-typeahead/actor-typeahead.js" "$OUT/" + +(cd "$REPO_ROOT" && go build -o "$TMP/dolly" ./cmd/dolly) +TEMPLATE="$REPO_ROOT/appview/pages/templates/fragments/dolly/logo.html" +"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.png" -size 180x180 +"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.ico" -size 48x48 +"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.svg" -color currentColor diff --git a/localinfra/scripts/init-accounts.sh b/localinfra/scripts/init-accounts.sh new file mode 100644 --- /dev/null +++ b/localinfra/scripts/init-accounts.sh @@ -0,0 +1,191 @@ +#!/bin/sh +# dev bootstrap: +# - create accounts (alice, bob) +# - write OWNER_DID to /shared/owner-did (for knot/spindle) +# - create system label definitions under SYSTEM_DID +set -eu + +: "${PDS_URL:?PDS_URL must be set}" +PASSWORD="password" + +USERS="alice bob" +OWNER_USER="${OWNER_USER:-alice}" +SYSTEM_USER="${SYSTEM_USER:-alice}" +SHARED_DIR="${SHARED_DIR:-/shared}" + +# --- helpers --- + +# resolve_handle HANDLE → DID on stdout +resolve_handle() { + resp=$(curl -sS -w '\n%{http_code}' \ + "${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=$1") + body=$(printf '%s\n' "$resp" | sed '$d') + status=$(printf '%s\n' "$resp" | tail -n1) + case "$status" in + 200) printf '%s\n' "$body" | jq -er '.did' ;; + 400) : ;; # not found — expected + *) printf 'resolveHandle %s: HTTP %s: %s\n' "$1" "$status" "$body" >&2; return 1 ;; + esac +} + +# ensure_account USERNAME → DID on stdout. Creates account if missing. +ensure_account() { + username="$1" + handle="${username}.${PDS_HOSTNAME}" + email="${username}@${PDS_HOSTNAME}" + + did=$(resolve_handle "$handle") + if [ -n "$did" ]; then + printf '[skip] %s = %s\n' "$handle" "$did" >&2 + printf '%s\n' "$did" + return 0 + fi + + invite=$(curl -fsS -u "admin:${PDS_ADMIN_PASSWORD}" \ + -H "Content-Type: application/json" \ + -d '{"useCount":1}' \ + "${PDS_URL}/xrpc/com.atproto.server.createInviteCode" | jq -er '.code') + + result=$(curl -fsS \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"${email}\",\"handle\":\"${handle}\",\"password\":\"${PASSWORD}\",\"inviteCode\":\"${invite}\"}" \ + "${PDS_URL}/xrpc/com.atproto.server.createAccount") + + did=$(printf '%s\n' "$result" | jq -er '.did') + printf '[create] %s = %s (password: %s)\n' "$handle" "$did" "$PASSWORD" >&2 + printf '%s\n' "$did" +} + +# login DID/Handle → access JWT on stdout +login() { + curl -fsS -H "Content-Type: application/json" \ + -d "{\"identifier\":\"$1\",\"password\":\"${PASSWORD}\"}" \ + "${PDS_URL}/xrpc/com.atproto.server.createSession" \ + | jq -er '.accessJwt' +} + +# put_record JWT DID COLLECTION RKEY RECORD_JSON +put_record() { + jwt="$1"; did="$2"; collection="$3"; rkey="$4"; record="$5" + + payload=$(jq -nc \ + --arg repo "$did" \ + --arg collection "$collection" \ + --arg rkey "$rkey" \ + --argjson record "$record" \ + '{repo:$repo, collection:$collection, rkey:$rkey, record:$record}') + + curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${jwt}" \ + -d "$payload" \ + "${PDS_URL}/xrpc/com.atproto.repo.putRecord" >/dev/null + + printf '[record] at://%s/%s/%s\n' "$did" "$collection" "$rkey" >&2 +} + +# ensure accounts +OWNER_DID="" +SYSTEM_DID="" +for u in $USERS; do + did=$(ensure_account "$u") + if [ "$u" = "$OWNER_USER" ]; then + OWNER_DID="$did" + fi + if [ "$u" = "$SYSTEM_USER" ]; then + SYSTEM_DID="$did" + fi +done + +[ -n "$OWNER_DID" ] || { printf 'OWNER_USER %s not in USERS list\n' "$OWNER_USER" >&2; exit 1; } +[ -n "$SYSTEM_DID" ] || { printf 'SYSTEM_USER %s not in USERS list\n' "$SYSTEM_USER" >&2; exit 1; } + +mkdir -p "$SHARED_DIR" +printf '%s' "$OWNER_DID" > "${SHARED_DIR}/owner-did" +printf '[owner] %s → %s/owner-did\n' "$OWNER_USER" "$SHARED_DIR" >&2 +printf '%s' "$SYSTEM_DID" > "${SHARED_DIR}/system-did" +printf '[system] %s → %s/system-did\n' "$SYSTEM_USER" "$SHARED_DIR" >&2 + +# label definitions (under SYSTEM_DID) +JWT=$(login "$SYSTEM_DID") + +CREATED_AT="2025-09-22T11:14:35+01:00" + +put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "wontfix" "$(cat < "${SHARED_DIR}/label-gfi" +printf '%s' "$LABEL_DEFAULTS" > "${SHARED_DIR}/label-defaults" +printf '[env] wrote label-defaults, label-gfi\n' >&2 + +# service definitions (under OWNER_DID) +JWT=$(login "$OWNER_DID") + +put_record "$JWT" "$OWNER_DID" "sh.tangled.knot" $KNOT_HOSTNAME "{\"createdAt\": \"${CREATED_AT}\"}" + +printf 'done.\n' >&2 -- tangled.sh