1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
#!/usr/bin/env bash
set -euo pipefail
# Stand up a second AT Protocol PDS on the production host, at
# pds2.socialweb.computer, and create one test account on it.
#
# ./scripts/deploy-pds2.sh --dry-run
# ./scripts/deploy-pds2.sh
#
# Why a second PDS at all: the k8s door's node contracts are written into the
# market's own collections (com.publicdomainrelay.temp.*). A public PDS refuses
# records in a collection it does not know, so the identity that drives the door
# has to live on a PDS we run. This script produces that identity.
#
# WHY THE CODE COMES FROM THE HOST'S OWN TREE
#
# The unit runs ts/hono-pds/main.ts out of a socialweb-computer-kcp checkout. Its
# @publicdomainrelay/* imports resolve through the root deno.json workspace, so
# the shipped set is that workspace: deno.json, deno.lock, ts/ and
# socialweb-computer-k8s/.
#
# The tree it is copied from is a variable (SOURCE_ROOT, default
# /opt/socialweb-computer-kcp) and it is the one already on this host. That is not a shortcut: the version of
# hono-pds in this repo keeps accounts in a `new Map`
# (ts/hono-pds/lib/atproto-repo-deno/account-store.ts), so every restart would erase the
# account and every re-run of this script would mint a second did:plc for one
# handle. The tree already on the host persists accounts to Deno KV
# (accountStorage in main.ts, "accounts" prefix in account-store.ts). A tree
# without that wiring is refused below rather than deployed.
#
# WHY THE HANDLE NEEDS NO DNS RECORD TO RESOLVE
#
# A handle proves itself to a client either by a DNS TXT at _atproto.<handle> or
# by https://<handle>/.well-known/atproto-did. This PDS answers that path with
# its OWN did (hono-pds factory.ts), not with the account's, so the HTTPS method
# cannot come from the PDS process. It comes from Caddy instead: a second site
# block for the handle's hostname answers that one path with the account's did
# and proxies everything else to the PDS. *.socialweb.computer already resolves
# here, so Caddy can get a certificate for the handle host over HTTP-01 and no
# operator action is needed. The equivalent TXT record is printed at the end
# anyway, because the DNS method is the one that survives this site block being
# removed.
#
# NOTHING HERE TOUCHES AN EXISTING UNIT. The only shared unit it reloads is
# Caddy, and only through `caddy validate` + `systemctl reload` after adding a
# drop-in under /etc/caddy/sites/. Every other unit on the host is left alone.
SSH_TARGET="${SSH_TARGET:-root@socialweb.computer}"
SSH_PORT="${SSH_PORT:-1997}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_no_password}"
SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o BatchMode=yes -o ConnectTimeout=20
-o ServerAliveInterval=15)
SERVICE_NAME="pds2"
SERVICE_USER="pds2"
ORIGIN="pds2.socialweb.computer"
HANDLE="test.pds2.socialweb.computer"
ACCOUNT_EMAIL="test@pds2.socialweb.computer"
PORT="8794"
PREFIX="/opt/socialweb-pds2"
SOURCE_ROOT="/opt/socialweb-computer-kcp"
ENTRY_CONFIG="deno.json"
ENTRYPOINT="ts/hono-pds/main.ts"
STATE_DIR="/var/lib/pds2"
DENO="/usr/local/bin/deno"
PLC_DIRECTORY_URL="https://plc.directory"
BIND="127.0.0.1"
PROBE_COLLECTION="com.publicdomainrelay.temp.badgeBlueKeys"
CADDY_SITES_DIR="/etc/caddy/sites"
CADDYFILE="/etc/caddy/Caddyfile"
ENV_FILE="${STATE_DIR}/pds.env"
CONFIG_PATH="${STATE_DIR}/config.json"
PASSWORD_FILE="${STATE_DIR}/account-password"
DID_FILE="${STATE_DIR}/account-did"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
-h|--help) sed -n '2,45p' "$0"; exit 0 ;;
*) echo "unknown argument: $arg" >&2; exit 2 ;;
esac
done
say() { printf '\n=== %s ===\n' "$*"; }
# Every remote command goes through here, so --dry-run is one branch rather than
# a flag threaded through every step.
remote() {
if ${DRY_RUN}; then
printf -- '--- [dry-run] ssh %s -p %s %s\n' "${SSH_TARGET}" "${SSH_PORT}" "$*"
else
ssh "${SSH_OPTS[@]}" -i "${SSH_KEY}" -p "${SSH_PORT}" "${SSH_TARGET}" "$@"
fi
}
# The local values are handed to the remote script as shell assignments so the
# previewed script reads the way it will run.
REMOTE_VARS="$(cat <<EOF
SERVICE_NAME='${SERVICE_NAME}'
SERVICE_USER='${SERVICE_USER}'
ORIGIN='${ORIGIN}'
HANDLE='${HANDLE}'
ACCOUNT_EMAIL='${ACCOUNT_EMAIL}'
PORT='${PORT}'
PREFIX='${PREFIX}'
SOURCE_ROOT='${SOURCE_ROOT}'
ENTRY_CONFIG='${ENTRY_CONFIG}'
ENTRYPOINT='${ENTRYPOINT}'
STATE_DIR='${STATE_DIR}'
DENO='${DENO}'
PLC_DIRECTORY_URL='${PLC_DIRECTORY_URL}'
BIND='${BIND}'
PROBE_COLLECTION='${PROBE_COLLECTION}'
CADDY_SITES_DIR='${CADDY_SITES_DIR}'
CADDYFILE='${CADDYFILE}'
ENV_FILE='${ENV_FILE}'
CONFIG_PATH='${CONFIG_PATH}'
PASSWORD_FILE='${PASSWORD_FILE}'
DID_FILE='${DID_FILE}'
EOF
)"
# A multi-line remote script, read from stdin.
remote_script() {
local desc="$1"
local body full
body="$(cat)"
full="${REMOTE_VARS}
${body}"
if ${DRY_RUN}; then
printf -- '--- [dry-run] ssh %s -p %s %s <<REMOTE\n%s\n--- end\n' \
"${SSH_TARGET}" "${SSH_PORT}" "${desc}" "${full}"
else
printf '%s\n' "${full}" | ssh "${SSH_OPTS[@]}" -i "${SSH_KEY}" -p "${SSH_PORT}" \
"${SSH_TARGET}" "bash -s"
fi
}
# ── Host preflight ────────────────────────────────────────────────────────────
say "checking ${SSH_TARGET}"
remote 'test "$(id -u)" = 0 && echo "root on $(hostname)"'
# ── The PDS answers on the loopback port before Caddy is told about it ────────
say "shipping the PDS workspace to ${PREFIX} and starting ${SERVICE_NAME}"
remote_script "install ${SERVICE_NAME}" <<REMOTE
set -euo pipefail
for tool in rsync openssl curl jq useradd runuser; do
command -v "\$tool" >/dev/null 2>&1 || { echo "missing on the host: \$tool" >&2; exit 1; }
done
[ -x "\${DENO}" ] || { echo "no deno at \${DENO}" >&2; exit 1; }
[ -f "\${SOURCE_ROOT}/\${ENTRY_CONFIG}" ] && [ -f "\${SOURCE_ROOT}/\${ENTRYPOINT}" ] || {
echo "no socialweb-computer-kcp checkout at \${SOURCE_ROOT}" >&2; exit 1; }
install -d -m 0755 "\${PREFIX}"
cp "\${SOURCE_ROOT}/deno.json" "\${SOURCE_ROOT}/deno.lock" "\${PREFIX}/"
for tree in ts socialweb-computer-k8s; do
rsync -a --delete --exclude node_modules --exclude web/downloads "\${SOURCE_ROOT}/\${tree}/" "\${PREFIX}/\${tree}/"
done
# Refuse a tree whose accounts do not survive a restart. See the header.
grep -q 'accountStorage' "\${PREFIX}/\${ENTRYPOINT}" || {
echo "\${PREFIX}/\${ENTRYPOINT} keeps accounts in memory (no accountStorage)." >&2
echo "Accounts would vanish on every restart and every re-run of this deploy would" >&2
echo "mint a second did:plc for \${HANDLE}. Ship a tree that persists them." >&2
exit 1
}
id -u "\${SERVICE_USER}" >/dev/null 2>&1 || \
useradd --system --create-home --home-dir "\${STATE_DIR}" --shell /usr/sbin/nologin "\${SERVICE_USER}"
install -d -m 0700 -o "\${SERVICE_USER}" -g "\${SERVICE_USER}" "\${STATE_DIR}"
# The PDS key is the issuer of every access token it mints, and the admin
# password gates createAccount (without it the route is unauthenticated). Both
# are generated once and kept: regenerating either invalidates sessions or
# reopens account creation.
if [ ! -f "\${ENV_FILE}" ]; then
umask 077
{
printf 'PDS_PRIVATE_KEY_HEX=%s\n' "\$(openssl rand -hex 32)"
printf 'PDS_ADMIN_PASSWORD=%s\n' "\$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)"
printf 'PDS_STORAGE_PATH=%s\n' "\${STATE_DIR}/pds.kv"
} > "\${ENV_FILE}"
echo "generated \${ENV_FILE}"
fi
chmod 0600 "\${ENV_FILE}"
chown "\${SERVICE_USER}:\${SERVICE_USER}" "\${ENV_FILE}"
# The CLI reads config.json from CONFIG_PATH_HONO_PDS. Keeping it in the state
# dir leaves the shipped tree read-only.
if [ ! -f "\${CONFIG_PATH}" ]; then
printf '{\n "crawlers": ""\n}\n' > "\${CONFIG_PATH}"
fi
chown "\${SERVICE_USER}:\${SERVICE_USER}" "\${CONFIG_PATH}"
chmod 0644 "\${CONFIG_PATH}"
# Deno updates the lockfile next to its config whenever the resolved
# dependencies move, and the shipped tree is read-only -- that is the point of
# ProtectSystem=strict, and on a host where the sibling copy of this tree is
# writable deno would never have complained. So the lock lives in the state dir:
# copied from the tree on first run, updated there afterwards, and pointed at
# with --lock. Pinning survives; the tree stays read-only.
if [ ! -f "\${STATE_DIR}/deno.lock" ]; then
cp "\${PREFIX}/deno.lock" "\${STATE_DIR}/deno.lock"
fi
chown "\${SERVICE_USER}:\${SERVICE_USER}" "\${STATE_DIR}/deno.lock"
chmod 0644 "\${STATE_DIR}/deno.lock"
cat > "\${STATE_DIR}/\${SERVICE_NAME}.service.new" <<UNIT
[Unit]
Description=\${SERVICE_NAME} — AT Protocol PDS for \${ORIGIN}
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=\${SERVICE_USER}
Group=\${SERVICE_USER}
WorkingDirectory=\${STATE_DIR}
EnvironmentFile=\${ENV_FILE}
Environment=HOME=\${STATE_DIR}
Environment=DENO_DIR=\${STATE_DIR}/deno
Environment=CONFIG_PATH_HONO_PDS=\${CONFIG_PATH}
# --node-modules-dir=none is what lets this run from a source tree that is never
# written: npm deps resolve out of DENO_DIR instead of a node_modules beside
# deno.json. One line, so that neither heredoc level has to carry an escape.
ExecStart=\${DENO} run -A --node-modules-dir=none --unstable-kv --lock \${STATE_DIR}/deno.lock --config \${PREFIX}/\${ENTRY_CONFIG} \${PREFIX}/\${ENTRYPOINT} --port \${PORT} --hostname \${BIND} --public-hostname \${ORIGIN} --plc-directory-url \${PLC_DIRECTORY_URL}
Restart=always
RestartSec=5
LimitNOFILE=65536
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
ReadWritePaths=\${STATE_DIR}
[Install]
WantedBy=multi-user.target
UNIT
install -d -m 0700 -o "\${SERVICE_USER}" -g "\${SERVICE_USER}" "\${STATE_DIR}/deno"
runuser -u "\${SERVICE_USER}" -- env HOME="\${STATE_DIR}" DENO_DIR="\${STATE_DIR}/deno" \\
"\${DENO}" cache --node-modules-dir=none --unstable-kv --lock "\${STATE_DIR}/deno.lock" \\
--config "\${PREFIX}/\${ENTRY_CONFIG}" "\${PREFIX}/\${ENTRYPOINT}" >/dev/null 2>&1 || true
changed=no
if cmp -s "\${STATE_DIR}/\${SERVICE_NAME}.service.new" "/etc/systemd/system/\${SERVICE_NAME}.service"; then
rm -f "\${STATE_DIR}/\${SERVICE_NAME}.service.new"
else
mv "\${STATE_DIR}/\${SERVICE_NAME}.service.new" "/etc/systemd/system/\${SERVICE_NAME}.service"
chmod 644 "/etc/systemd/system/\${SERVICE_NAME}.service"
changed=yes
fi
systemctl daemon-reload
systemctl enable "\${SERVICE_NAME}" >/dev/null
if [ "\${changed}" = yes ]; then
systemctl restart "\${SERVICE_NAME}"
echo "unit changed; restarted \${SERVICE_NAME}"
elif ! systemctl is-active --quiet "\${SERVICE_NAME}"; then
systemctl start "\${SERVICE_NAME}"
echo "unit unchanged; started \${SERVICE_NAME}"
else
echo "unit unchanged and running; left alone"
fi
for _ in \$(seq 1 60); do
if curl -fsS "http://\${BIND}:\${PORT}/xrpc/_health" >/dev/null 2>&1; then
echo "health on \${BIND}:\${PORT} ok"
exit 0
fi
sleep 2
done
echo "\${SERVICE_NAME} never answered on \${BIND}:\${PORT}; last log lines:" >&2
journalctl -u "\${SERVICE_NAME}" -n 40 --no-pager >&2 || true
exit 1
REMOTE
# ── Caddy: the origin, then the account ───────────────────────────────────────
say "pointing ${ORIGIN} at the PDS and reloading Caddy"
remote_script "caddy site for ${ORIGIN}" <<REMOTE
set -euo pipefail
install -d -m 0755 "\${CADDY_SITES_DIR}"
cat > "\${CADDY_SITES_DIR}/\${ORIGIN}.caddy" <<SITE
\${ORIGIN} {
reverse_proxy http://\${BIND}:\${PORT}
}
SITE
# The drop-in directory is imported by the Caddyfile. Adding the import if it is
# somehow absent, once, with the backup the sibling deploys take.
if ! grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites/\*\.caddy' "\${CADDYFILE}"; then
cp "\${CADDYFILE}" "\${CADDYFILE}.bak.\$(date +%s)"
printf '\nimport /etc/caddy/sites/*.caddy\n' >> "\${CADDYFILE}"
fi
if ! grep -qE '^[[:space:]]*skip_install_trust[[:space:]]*\$' "\${CADDYFILE}"; then
cp "\${CADDYFILE}" "\${CADDYFILE}.bak.\$(date +%s).trust"
if [ "\$(grep -vE '^[[:space:]]*(#|\$)' "\${CADDYFILE}" | awk 'NR==1' | tr -d '[:space:]')" = "{" ]; then
awk '!done && /^[[:space:]]*\{[[:space:]]*\$/ { print; print "\tskip_install_trust"; done = 1; next } { print }' "\${CADDYFILE}" > "\${CADDYFILE}.trust"
else
{ printf '{\n\tskip_install_trust\n}\n\n'; cat "\${CADDYFILE}"; } > "\${CADDYFILE}.trust"
fi
mv "\${CADDYFILE}.trust" "\${CADDYFILE}"
fi
caddy validate --config "\${CADDYFILE}" >/dev/null
systemctl reload caddy || systemctl restart caddy
for _ in \$(seq 1 45); do
if curl -fsS "https://\${ORIGIN}/xrpc/_health" >/dev/null 2>&1; then
echo "https://\${ORIGIN} answers (certificate obtained)"
exit 0
fi
sleep 2
done
echo "https://\${ORIGIN} did not answer within 90s" >&2
journalctl -u caddy -n 30 --no-pager >&2 || true
exit 1
REMOTE
# ── The account ───────────────────────────────────────────────────────────────
say "creating the test account if it is not already there"
remote_script "create account ${HANDLE}" <<REMOTE
set -euo pipefail
if [ ! -f "\${PASSWORD_FILE}" ]; then
umask 077
openssl rand -base64 24 | tr -d '/+=' | cut -c1-24 > "\${PASSWORD_FILE}"
fi
chmod 0600 "\${PASSWORD_FILE}"
PW="\$(cat "\${PASSWORD_FILE}")"
ADMIN_B64="\$(grep '^PDS_ADMIN_PASSWORD=' "\${ENV_FILE}" | cut -d= -f2- | tr -d '\n' | { printf 'admin:'; cat; } | base64 -w0)"
session="\$(curl -fsS -X POST "https://\${ORIGIN}/xrpc/com.atproto.server.createSession" \\
-H 'content-type: application/json' \\
--data "{\\"identifier\\":\\"\${HANDLE}\\",\\"password\\":\\"\${PW}\\"}" 2>/dev/null || true)"
did="\$(printf '%s' "\${session}" | jq -r '.did // empty')"
if [ -z "\${did}" ]; then
if [ -f "\${DID_FILE}" ]; then
echo "an account for \${HANDLE} was already created on this PDS but the stored" >&2
echo "password no longer authenticates it. Refusing to create a second account" >&2
echo "for one handle. To start over: systemctl stop \${SERVICE_NAME}, remove" >&2
echo "\${DID_FILE}, \${PASSWORD_FILE} and \${STATE_DIR}/pds.kv, then re-run." >&2
exit 1
fi
created="\$(curl -fsS -X POST "https://\${ORIGIN}/xrpc/com.atproto.server.createAccount" \\
-H "authorization: Basic \${ADMIN_B64}" \\
-H 'content-type: application/json' \\
--data "{\\"handle\\":\\"\${HANDLE}\\",\\"email\\":\\"\${ACCOUNT_EMAIL}\\",\\"password\\":\\"\${PW}\\"}")"
did="\$(printf '%s' "\${created}" | jq -r '.did')"
echo "created \${HANDLE} as \${did}"
else
echo "\${HANDLE} already exists as \${did}"
fi
printf '%s' "\${did}" > "\${DID_FILE}"
chmod 0644 "\${DID_FILE}"
echo "password is in \${PASSWORD_FILE} (0600), not printed here"
REMOTE
# ── The handle resolves without a DNS record ──────────────────────────────────
say "serving the handle's did at https://${HANDLE}/.well-known/atproto-did"
remote_script "caddy site for ${HANDLE}" <<REMOTE
set -euo pipefail
cat > "\${CADDY_SITES_DIR}/\${HANDLE}.caddy" <<SITE
\${HANDLE} {
handle /.well-known/atproto-did {
respond "\$(cat \${DID_FILE})" 200
}
handle {
reverse_proxy http://\${BIND}:\${PORT}
}
}
SITE
caddy validate --config "\${CADDYFILE}" >/dev/null
systemctl reload caddy || systemctl restart caddy
resolved=''
for _ in \$(seq 1 45); do
resolved="\$(curl -fsS "https://\${HANDLE}/.well-known/atproto-did" 2>/dev/null || true)"
[ "\${resolved}" = "\$(cat \${DID_FILE})" ] && { echo "handle resolves to \${resolved}"; exit 0; }
sleep 2
done
echo "warning: https://\${HANDLE}/.well-known/atproto-did did not come up." >&2
echo "The PDS itself is up; a client that resolves \${HANDLE} by DNS still needs the" >&2
echo "_atproto TXT record printed at the end of this deploy." >&2
exit 0
REMOTE
# ── Prove it: a session, and a write into a collection no public PDS takes ────
say "verifying: describeServer, session, createRecord"
remote_script "verify ${SERVICE_NAME}" <<REMOTE
set -euo pipefail
echo "--- GET https://\${ORIGIN}/xrpc/_health"
curl -fsS "https://\${ORIGIN}/xrpc/_health"; echo
echo "--- GET https://\${ORIGIN}/xrpc/com.atproto.server.describeServer"
curl -fsS "https://\${ORIGIN}/xrpc/com.atproto.server.describeServer"; echo
echo "--- GET https://\${HANDLE}/.well-known/atproto-did"
curl -fsS "https://\${HANDLE}/.well-known/atproto-did" || echo "(not served)"
echo
echo "--- POST createSession as \${HANDLE}"
PW="\$(cat "\${PASSWORD_FILE}")"
token="\$(curl -fsS -X POST "https://\${ORIGIN}/xrpc/com.atproto.server.createSession" \\
-H 'content-type: application/json' \\
--data "{\\"identifier\\":\\"\${HANDLE}\\",\\"password\\":\\"\${PW}\\"}" | jq -r '.accessJwt')"
[ -n "\${token}" ] && [ "\${token}" != null ] || { echo "createSession gave no token" >&2; exit 1; }
echo "session created"
echo "--- POST putRecord into \${PROBE_COLLECTION}"
record="{\\"repo\\":\\"self\\",\\"collection\\":\\"\${PROBE_COLLECTION}\\",\\"rkey\\":\\"deploy-check\\",\\"record\\":{\\"\\\$type\\":\\"\${PROBE_COLLECTION}\\",\\"note\\":\\"pds2 deploy probe\\",\\"createdAt\\":\\"\$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\\"}}"
curl -fsS -X POST "https://\${ORIGIN}/xrpc/com.atproto.repo.putRecord" \\
-H "authorization: Bearer \${token}" \\
-H 'content-type: application/json' \\
--data "\${record}"; echo
echo "--- GET listRecords \${PROBE_COLLECTION} for \$(cat \${DID_FILE})"
# Not repo=self: this PDS resolves "self" on the write routes only, so a read
# has to name the account the way a client would.
curl -fsS "https://\${ORIGIN}/xrpc/com.atproto.repo.listRecords?repo=\$(cat \${DID_FILE})&collection=\${PROBE_COLLECTION}" \\
-H "authorization: Bearer \${token}"; echo
REMOTE
# ── What an operator still has to know ────────────────────────────────────────
say "report"
remote_script "report" <<'REMOTE'
set -euo pipefail
echo "service ${SERVICE_NAME}.service ($(systemctl is-active ${SERVICE_NAME}))"
echo "listening ${BIND}:${PORT} (loopback only)"
echo "origin https://${ORIGIN}"
echo "handle ${HANDLE}"
echo "account $(cat ${DID_FILE})"
echo "password ${PASSWORD_FILE} (0600, root; never printed)"
echo "state ${STATE_DIR} (deno dir, config, pds.env, pds.kv, account-did)"
echo "code ${PREFIX} (the ${ENTRY_CONFIG} workspace, copied from ${SOURCE_ROOT})"
echo
echo "verify:"
echo " curl -fsS https://${ORIGIN}/xrpc/_health"
echo " curl -fsS https://${ORIGIN}/xrpc/com.atproto.server.describeServer"
echo " curl -fsS https://${HANDLE}/.well-known/atproto-did"
echo
echo "no DNS record is needed: https://${HANDLE}/.well-known/atproto-did answers"
echo "with the account's did, which is one of the two ways a client resolves a"
echo "handle. The DNS method is the other, and it is the one that still works if"
echo "that Caddy site block is ever removed -- add this record to take it:"
echo
echo " _atproto.${HANDLE}. IN TXT \"did=$(cat ${DID_FILE})\""
echo
echo "sign in at https://k8s.socialweb.computer with the handle above; the password"
echo "is in ${PASSWORD_FILE} on this host."
REMOTE
say "done"