+4
-7
appview/pages/pages.go
+4
-7
appview/pages/pages.go
···
612
612
}
613
613
614
614
type FollowFragmentParams struct {
615
-
UserDid string
616
-
FollowStatus models.FollowStatus
617
-
FollowersCount int64
615
+
UserDid string
616
+
FollowStatus models.FollowStatus
618
617
}
619
618
620
619
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
621
-
return p.executePlain("user/fragments/follow-oob", w, params)
620
+
return p.executePlain("user/fragments/follow", w, params)
622
621
}
623
622
624
623
type EditBioParams struct {
···
649
648
IsStarred bool
650
649
SubjectAt syntax.ATURI
651
650
StarCount int
652
-
HxSwapOob bool
653
651
}
654
652
655
653
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
656
-
params.HxSwapOob = true
657
-
return p.executePlain("fragments/starBtn", w, params)
654
+
return p.executePlain("fragments/starBtn-oob", w, params)
658
655
}
659
656
660
657
type RepoIndexParams struct {
+5
appview/pages/templates/fragments/starBtn-oob.html
+5
appview/pages/templates/fragments/starBtn-oob.html
-1
appview/pages/templates/fragments/starBtn.html
-1
appview/pages/templates/fragments/starBtn.html
-6
appview/pages/templates/user/fragments/follow-oob.html
-6
appview/pages/templates/user/fragments/follow-oob.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
···
9
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
11
<a href="/{{ $userIdent }}">
12
-
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{
13
-
$userIdent | truncateAt30 }}</span>
12
+
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
14
13
</a>
15
14
{{ with .Profile }}
16
15
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
17
16
{{ end }}
18
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
19
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
20
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
21
-
.FollowersCount }} followers</a></span>
19
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span>
22
20
<span class="select-none after:content-['ยท']"></span>
23
21
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span>
24
22
</div>
···
31
29
</div>
32
30
</div>
33
31
</div>
34
-
{{ end }}
32
+
{{ end }}
+99
-97
appview/pages/templates/user/fragments/profileCard.html
+99
-97
appview/pages/templates/user/fragments/profileCard.html
···
1
1
{{ define "user/fragments/profileCard" }}
2
-
{{ $userIdent := resolve .UserDid }}
3
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
-
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
-
<div class="w-3/4 aspect-square relative">
6
-
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
-
</div>
8
-
</div>
9
-
<div class="col-span-2">
10
-
<div class="flex items-center flex-row flex-nowrap gap-2">
11
-
<p title="{{ $userIdent }}"
12
-
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
-
{{ $userIdent }}
14
-
</p>
15
-
{{ with .Profile }}
16
-
{{ if .Pronouns }}
17
-
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
-
{{ end }}
19
-
{{ end }}
20
-
</div>
2
+
{{ $userIdent := resolve .UserDid }}
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
<div class="w-3/4 aspect-square relative">
6
+
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
+
</div>
8
+
</div>
9
+
<div class="col-span-2">
10
+
<div class="flex items-center flex-row flex-nowrap gap-2">
11
+
<p title="{{ $userIdent }}"
12
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
+
{{ $userIdent }}
14
+
</p>
15
+
{{ with .Profile }}
16
+
{{ if .Pronouns }}
17
+
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
+
{{ end }}
19
+
{{ end }}
20
+
</div>
21
21
22
-
<div class="md:hidden">
23
-
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
-
</div>
25
-
</div>
26
-
<div class="col-span-3 md:col-span-full">
27
-
<div id="profile-bio" class="text-sm">
28
-
{{ $profile := .Profile }}
29
-
{{ with .Profile }}
22
+
<div class="md:hidden">
23
+
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
+
</div>
25
+
</div>
26
+
<div class="col-span-3 md:col-span-full">
27
+
<div id="profile-bio" class="text-sm">
28
+
{{ $profile := .Profile }}
29
+
{{ with .Profile }}
30
30
31
-
{{ if .Description }}
32
-
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
-
{{ end }}
31
+
{{ if .Description }}
32
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
+
{{ end }}
34
34
35
-
<div class="hidden md:block">
36
-
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
-
</div>
35
+
<div class="hidden md:block">
36
+
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
+
</div>
38
38
39
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
-
{{ if .Location }}
41
-
<div class="flex items-center gap-2">
42
-
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
-
<span>{{ .Location }}</span>
44
-
</div>
45
-
{{ end }}
46
-
{{ if .IncludeBluesky }}
47
-
<div class="flex items-center gap-2">
48
-
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white"
49
-
}}</span>
50
-
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
51
-
</div>
52
-
{{ end }}
53
-
{{ range $link := .Links }}
54
-
{{ if $link }}
55
-
<div class="flex items-center gap-2">
56
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
57
-
<a href="{{ $link }}">{{ $link }}</a>
58
-
</div>
59
-
{{ end }}
60
-
{{ end }}
61
-
{{ if not $profile.IsStatsEmpty }}
62
-
<div class="flex items-center justify-evenly gap-2 py-2">
63
-
{{ range $stat := .Stats }}
64
-
{{ if $stat.Kind }}
65
-
<div class="flex flex-col items-center gap-2">
66
-
<span class="text-xl font-bold">{{ $stat.Value }}</span>
67
-
<span>{{ $stat.Kind.String }}</span>
39
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
+
{{ if .Location }}
41
+
<div class="flex items-center gap-2">
42
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
+
<span>{{ .Location }}</span>
44
+
</div>
45
+
{{ end }}
46
+
{{ if .IncludeBluesky }}
47
+
<div class="flex items-center gap-2">
48
+
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span>
49
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
50
+
</div>
51
+
{{ end }}
52
+
{{ range $link := .Links }}
53
+
{{ if $link }}
54
+
<div class="flex items-center gap-2">
55
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
56
+
<a href="{{ $link }}">{{ $link }}</a>
57
+
</div>
58
+
{{ end }}
59
+
{{ end }}
60
+
{{ if not $profile.IsStatsEmpty }}
61
+
<div class="flex items-center justify-evenly gap-2 py-2">
62
+
{{ range $stat := .Stats }}
63
+
{{ if $stat.Kind }}
64
+
<div class="flex flex-col items-center gap-2">
65
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
66
+
<span>{{ $stat.Kind.String }}</span>
67
+
</div>
68
+
{{ end }}
69
+
{{ end }}
70
+
</div>
71
+
{{ end }}
68
72
</div>
69
73
{{ end }}
70
-
{{ end }}
71
-
</div>
72
-
{{ end }}
73
-
</div>
74
-
{{ end }}
75
74
76
-
<div class="flex mt-2 items-center gap-2">
77
-
{{ if ne .FollowStatus.String "IsSelf" }}
78
-
{{ template "user/fragments/follow" . }}
79
-
{{ else }}
80
-
<button id="editBtn" class="btn w-full flex items-center gap-2 group" hx-target="#profile-bio"
81
-
hx-get="/profile/edit-bio" hx-swap="innerHTML">
82
-
{{ i "pencil" "w-4 h-4" }}
83
-
edit
84
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
85
-
</button>
86
-
{{ end }}
75
+
<div class="flex mt-2 items-center gap-2">
76
+
{{ if ne .FollowStatus.String "IsSelf" }}
77
+
{{ template "user/fragments/follow" . }}
78
+
{{ else }}
79
+
<button id="editBtn"
80
+
class="btn w-full flex items-center gap-2 group"
81
+
hx-target="#profile-bio"
82
+
hx-get="/profile/edit-bio"
83
+
hx-swap="innerHTML">
84
+
{{ i "pencil" "w-4 h-4" }}
85
+
edit
86
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
87
+
</button>
88
+
{{ end }}
87
89
88
-
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
89
-
href="/{{ $userIdent }}/feed.atom">
90
-
{{ i "rss" "size-4" }}
91
-
</a>
92
-
</div>
90
+
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
91
+
href="/{{ $userIdent }}/feed.atom">
92
+
{{ i "rss" "size-4" }}
93
+
</a>
94
+
</div>
93
95
96
+
</div>
97
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
98
+
</div>
94
99
</div>
95
-
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
96
-
</div>
97
-
</div>
98
100
{{ end }}
99
101
100
102
{{ define "followerFollowing" }}
101
-
{{ $root := index . 0 }}
102
-
{{ $userIdent := index . 1 }}
103
-
{{ with $root }}
104
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
105
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
106
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
107
-
.Stats.FollowersCount }} followers</a></span>
108
-
<span class="select-none after:content-['ยท']"></span>
109
-
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
110
-
</div>
103
+
{{ $root := index . 0 }}
104
+
{{ $userIdent := index . 1 }}
105
+
{{ with $root }}
106
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
107
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
108
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .Stats.FollowersCount }} followers</a></span>
109
+
<span class="select-none after:content-['ยท']"></span>
110
+
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
111
+
</div>
112
+
{{ end }}
111
113
{{ end }}
112
-
{{ end }}
114
+
+4
-16
appview/state/follow.go
+4
-16
appview/state/follow.go
···
75
75
76
76
s.notifier.NewFollow(r.Context(), follow)
77
77
78
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
79
-
if err != nil {
80
-
log.Println("failed to get follow stats", err)
81
-
}
82
-
83
78
s.pages.FollowFragment(w, pages.FollowFragmentParams{
84
-
UserDid: subjectIdent.DID.String(),
85
-
FollowStatus: models.IsFollowing,
86
-
FollowersCount: followStats.Followers,
79
+
UserDid: subjectIdent.DID.String(),
80
+
FollowStatus: models.IsFollowing,
87
81
})
88
82
89
83
return
···
112
106
// this is not an issue, the firehose event might have already done this
113
107
}
114
108
115
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
116
-
if err != nil {
117
-
log.Println("failed to get follow stats", err)
118
-
}
119
-
120
109
s.pages.FollowFragment(w, pages.FollowFragmentParams{
121
-
UserDid: subjectIdent.DID.String(),
122
-
FollowStatus: models.IsNotFollowing,
123
-
FollowersCount: followStats.Followers,
110
+
UserDid: subjectIdent.DID.String(),
111
+
FollowStatus: models.IsNotFollowing,
124
112
})
125
113
126
114
s.notifier.DeleteFollow(r.Context(), follow)
+11
contrib/certs/root.crt
+11
contrib/certs/root.crt
···
1
+
-----BEGIN CERTIFICATE-----
2
+
MIIBozCCAUmgAwIBAgIQRnYoKs3BuihlLFeydgURVzAKBggqhkjOPQQDAjAwMS4w
3
+
LAYDVQQDEyVDYWRkeSBMb2NhbCBBdXRob3JpdHkgLSAyMDI2IEVDQyBSb290MB4X
4
+
DTI2MDEwODEzNTk1MloXDTM1MTExNzEzNTk1MlowMDEuMCwGA1UEAxMlQ2FkZHkg
5
+
TG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDBZMBMGByqGSM49AgEGCCqG
6
+
SM49AwEHA0IABCQlYShhxLaX8/ZP7rcBtD5xL4u3wYMe77JS/lRFjjpAUGmJPxUE
7
+
ctsNvukG1hU4MeLMSqAEIqFWjs8dQBxLjGSjRTBDMA4GA1UdDwEB/wQEAwIBBjAS
8
+
BgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQ7Mt/6izTOOXCSWDS6HrwrqMDB
9
+
vzAKBggqhkjOPQQDAgNIADBFAiEA9QAYIuHR5qsGJ1JMZnuAAQpEwaqewhUICsKO
10
+
e2fWj4ACICPgj9Kh9++8FH5eVyDI1AD/BLwmMmiaqs1ojZT7QJqb
11
+
-----END CERTIFICATE-----
+31
contrib/example.env
+31
contrib/example.env
···
1
+
# NOTE: put actual DIDs here
2
+
alice_did=did:plc:alice-did
3
+
tangled_did=did:plc:tangled-did
4
+
5
+
#core
6
+
export TANGLED_DEV=true
7
+
export TANGLED_APPVIEW_HOST=http://127.0.0.1:3000
8
+
# plc
9
+
export TANGLED_PLC_URL=https://plc.tngl.boltless.dev
10
+
# jetstream
11
+
export TANGLED_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe
12
+
# label
13
+
export TANGLED_LABEL_GFI=at://${tangled_did}/sh.tangled.label.definition/good-first-issue
14
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_GFI
15
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/assignee
16
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/documentation
17
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/duplicate
18
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/wontfix
19
+
20
+
# vm settings
21
+
export TANGLED_VM_PLC_URL=https://plc.tngl.boltless.dev
22
+
export TANGLED_VM_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe
23
+
export TANGLED_VM_KNOT_HOST=knot.tngl.boltless.dev
24
+
export TANGLED_VM_KNOT_OWNER=$alice_did
25
+
export TANGLED_VM_SPINDLE_HOST=spindle.tngl.boltless.dev
26
+
export TANGLED_VM_SPINDLE_OWNER=$alice_did
27
+
28
+
if [ -n "${TANGLED_RESEND_API_KEY:-}" ] && [ -n "${TANGLED_RESEND_SENT_FROM:-}" ]; then
29
+
export TANGLED_VM_PDS_EMAIL_SMTP_URL=smtps://resend:$TANGLED_RESEND_API_KEY@smtp.resend.com:465/
30
+
export TANGLED_VM_PDS_EMAIL_FROM_ADDRESS=$TANGLED_RESEND_SENT_FROM
31
+
fi
+12
contrib/pds.env
+12
contrib/pds.env
···
1
+
LOG_ENABLED=true
2
+
3
+
PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a
4
+
PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3
5
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7
6
+
7
+
PDS_DATA_DIRECTORY=/pds
8
+
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
9
+
10
+
PDS_DID_PLC_URL=http://localhost:8080
11
+
PDS_HOSTNAME=pds.tngl.boltless.dev
12
+
PDS_PORT=3000
+25
contrib/readme.md
+25
contrib/readme.md
···
1
+
# how to setup local appview dev environment
2
+
3
+
Appview requires several microservices from knot and spindle to entire atproto infra. This test environment is implemented under nixos vm.
4
+
5
+
1. copy `contrib/example.env` to `.env`, fill it and source it
6
+
2. run vm
7
+
```bash
8
+
nix run --impure .#vm
9
+
```
10
+
3. trust the generated cert from host machine
11
+
```bash
12
+
# for macos
13
+
sudo security add-trusted-cert -d -r trustRoot \
14
+
-k /Library/Keychains/System.keychain \
15
+
./nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/root.crt
16
+
```
17
+
4. create test accounts with valid emails (use [`create-test-account.sh`](./scripts/create-test-account.sh))
18
+
5. create default labels (use [`setup-const-records`](./scripts/setup-const-records.sh))
19
+
6. restart vm with correct owner-did
20
+
21
+
for git-https, you should change your local git config:
22
+
```
23
+
[http "https://knot.tngl.boltless.dev"]
24
+
sslCAPath = /Users/boltless/repo/tangled/nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/
25
+
```
+68
contrib/scripts/create-test-account.sh
+68
contrib/scripts/create-test-account.sh
···
1
+
#!/bin/bash
2
+
set -o errexit
3
+
set -o nounset
4
+
set -o pipefail
5
+
6
+
source "$(dirname "$0")/../pds.env"
7
+
8
+
# PDS_HOSTNAME=
9
+
# PDS_ADMIN_PASSWORD=
10
+
11
+
# curl a URL and fail if the request fails.
12
+
function curl_cmd_get {
13
+
curl --fail --silent --show-error "$@"
14
+
}
15
+
16
+
# curl a URL and fail if the request fails.
17
+
function curl_cmd_post {
18
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
19
+
}
20
+
21
+
# curl a URL but do not fail if the request fails.
22
+
function curl_cmd_post_nofail {
23
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
24
+
}
25
+
26
+
USERNAME="${1:-}"
27
+
28
+
if [[ "${USERNAME}" == "" ]]; then
29
+
read -p "Enter a username: " USERNAME
30
+
fi
31
+
32
+
if [[ "${USERNAME}" == "" ]]; then
33
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
34
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
35
+
exit 1
36
+
fi
37
+
38
+
EMAIL=${USERNAME}@${PDS_HOSTNAME}
39
+
40
+
PASSWORD="password"
41
+
INVITE_CODE="$(curl_cmd_post \
42
+
--user "admin:${PDS_ADMIN_PASSWORD}" \
43
+
--data '{"useCount": 1}' \
44
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code'
45
+
)"
46
+
RESULT="$(curl_cmd_post_nofail \
47
+
--data "{\"email\":\"${EMAIL}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \
48
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount"
49
+
)"
50
+
51
+
DID="$(echo $RESULT | jq --raw-output '.did')"
52
+
if [[ "${DID}" != did:* ]]; then
53
+
ERR="$(echo ${RESULT} | jq --raw-output '.message')"
54
+
echo "ERROR: ${ERR}" >/dev/stderr
55
+
echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr
56
+
exit 1
57
+
fi
58
+
59
+
echo
60
+
echo "Account created successfully!"
61
+
echo "-----------------------------"
62
+
echo "Handle : ${USERNAME}.${PDS_HOSTNAME}"
63
+
echo "DID : ${DID}"
64
+
echo "Password : ${PASSWORD}"
65
+
echo "-----------------------------"
66
+
echo "This is a test account with an insecure password."
67
+
echo "Make sure it's only used for development."
68
+
echo
+106
contrib/scripts/setup-const-records.sh
+106
contrib/scripts/setup-const-records.sh
···
1
+
#!/bin/bash
2
+
set -o errexit
3
+
set -o nounset
4
+
set -o pipefail
5
+
6
+
source "$(dirname "$0")/../pds.env"
7
+
8
+
# PDS_HOSTNAME=
9
+
10
+
# curl a URL and fail if the request fails.
11
+
function curl_cmd_get {
12
+
curl --fail --silent --show-error "$@"
13
+
}
14
+
15
+
# curl a URL and fail if the request fails.
16
+
function curl_cmd_post {
17
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
18
+
}
19
+
20
+
# curl a URL but do not fail if the request fails.
21
+
function curl_cmd_post_nofail {
22
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
23
+
}
24
+
25
+
USERNAME="${1:-}"
26
+
27
+
if [[ "${USERNAME}" == "" ]]; then
28
+
read -p "Enter a username: " USERNAME
29
+
fi
30
+
31
+
if [[ "${USERNAME}" == "" ]]; then
32
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
33
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
34
+
exit 1
35
+
fi
36
+
37
+
SESS_RESULT="$(curl_cmd_post \
38
+
--data "$(cat <<EOF
39
+
{
40
+
"identifier": "$USERNAME",
41
+
"password": "password"
42
+
}
43
+
EOF
44
+
)" \
45
+
https://pds.tngl.boltless.dev/xrpc/com.atproto.server.createSession
46
+
)"
47
+
48
+
echo $SESS_RESULT | jq
49
+
50
+
DID="$(echo $SESS_RESULT | jq --raw-output '.did')"
51
+
ACCESS_JWT="$(echo $SESS_RESULT | jq --raw-output '.accessJwt')"
52
+
53
+
function add_label_def {
54
+
local color=$1
55
+
local name=$2
56
+
echo $color
57
+
echo $name
58
+
local json_payload=$(cat <<EOF
59
+
{
60
+
"repo": "$DID",
61
+
"collection": "sh.tangled.label.definition",
62
+
"rkey": "$name",
63
+
"record": {
64
+
"name": "$name",
65
+
"color": "$color",
66
+
"scope": ["sh.tangled.repo.issue"],
67
+
"multiple": false,
68
+
"createdAt": "2025-09-22T11:14:35+01:00",
69
+
"valueType": {"type": "null", "format": "any"}
70
+
}
71
+
}
72
+
EOF
73
+
)
74
+
echo $json_payload
75
+
echo $json_payload | jq
76
+
RESULT="$(curl_cmd_post \
77
+
--data "$json_payload" \
78
+
-H "Authorization: Bearer ${ACCESS_JWT}" \
79
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord")"
80
+
echo $RESULT | jq
81
+
}
82
+
83
+
add_label_def '#64748b' 'wontfix'
84
+
add_label_def '#8B5CF6' 'good-first-issue'
85
+
add_label_def '#ef4444' 'duplicate'
86
+
add_label_def '#06b6d4' 'documentation'
87
+
json_payload=$(cat <<EOF
88
+
{
89
+
"repo": "$DID",
90
+
"collection": "sh.tangled.label.definition",
91
+
"rkey": "assignee",
92
+
"record": {
93
+
"name": "assignee",
94
+
"color": "#10B981",
95
+
"scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"],
96
+
"multiple": false,
97
+
"createdAt": "2025-09-22T11:14:35+01:00",
98
+
"valueType": {"type": "string", "format": "did"}
99
+
}
100
+
}
101
+
EOF
102
+
)
103
+
curl_cmd_post \
104
+
--data "$json_payload" \
105
+
-H "Authorization: Bearer ${ACCESS_JWT}" \
106
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord"
-3
docs/DOCS.md
-3
docs/DOCS.md
+34
-2
flake.nix
+34
-2
flake.nix
···
95
95
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
96
96
knot = self.callPackage ./nix/pkgs/knot.nix {};
97
97
dolly = self.callPackage ./nix/pkgs/dolly.nix {};
98
+
did-method-plc = self.callPackage ./nix/pkgs/did-method-plc.nix {};
99
+
bluesky-jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {};
100
+
bluesky-relay = self.callPackage ./nix/pkgs/bluesky-relay.nix {};
101
+
tap = self.callPackage ./nix/pkgs/tap.nix {};
98
102
});
99
103
in {
100
104
overlays.default = final: prev: {
101
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly;
105
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly did-method-plc bluesky-jetstream bluesky-relay tap;
102
106
};
103
107
104
108
packages = forAllSystems (system: let
···
119
123
sqlite-lib
120
124
docs
121
125
dolly
126
+
did-method-plc
127
+
bluesky-jetstream
128
+
bluesky-relay
129
+
tap
122
130
;
123
131
124
132
pkgsStatic-appview = staticPackages.appview;
···
248
256
rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1)
249
257
cd "$rootDir"
250
258
251
-
mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs}
259
+
mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs}
252
260
253
261
export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data"
254
262
exec ${pkgs.lib.getExe
···
320
328
imports = [./nix/modules/spindle.nix];
321
329
322
330
services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle;
331
+
};
332
+
nixosModules.did-method-plc = {
333
+
lib,
334
+
pkgs,
335
+
...
336
+
}: {
337
+
imports = [./nix/modules/did-method-plc.nix];
338
+
services.did-method-plc.package = lib.mkDefault self.packages.${pkgs.system}.did-method-plc;
339
+
};
340
+
nixosModules.bluesky-relay = {
341
+
lib,
342
+
pkgs,
343
+
...
344
+
}: {
345
+
imports = [./nix/modules/bluesky-relay.nix];
346
+
services.bluesky-relay.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-relay;
347
+
};
348
+
nixosModules.bluesky-jetstream = {
349
+
lib,
350
+
pkgs,
351
+
...
352
+
}: {
353
+
imports = [./nix/modules/bluesky-jetstream.nix];
354
+
services.bluesky-jetstream.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-jetstream;
323
355
};
324
356
};
325
357
}
-4
input.css
-4
input.css
-3
nix/modules/appview.nix
-3
nix/modules/appview.nix
···
1
1
{
2
-
pkgs,
3
2
config,
4
3
lib,
5
4
...
···
260
259
after = ["redis-appview.service" "network-online.target"];
261
260
requires = ["redis-appview.service"];
262
261
wants = ["network-online.target"];
263
-
264
-
path = [pkgs.diffutils];
265
262
266
263
serviceConfig = {
267
264
Type = "simple";
+64
nix/modules/bluesky-jetstream.nix
+64
nix/modules/bluesky-jetstream.nix
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.bluesky-jetstream;
8
+
in
9
+
with lib; {
10
+
options.services.bluesky-jetstream = {
11
+
enable = mkEnableOption "jetstream server";
12
+
package = mkPackageOption pkgs "bluesky-jetstream" {};
13
+
14
+
# dataDir = mkOption {
15
+
# type = types.str;
16
+
# default = "/var/lib/jetstream";
17
+
# description = "directory to store data (pebbleDB)";
18
+
# };
19
+
livenessTtl = mkOption {
20
+
type = types.int;
21
+
default = 15;
22
+
description = "time to restart when no event detected (seconds)";
23
+
};
24
+
websocketUrl = mkOption {
25
+
type = types.str;
26
+
default = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos";
27
+
description = "full websocket path to the ATProto SubscribeRepos XRPC endpoint";
28
+
};
29
+
};
30
+
config = mkIf cfg.enable {
31
+
systemd.services.bluesky-jetstream = {
32
+
description = "bluesky jetstream";
33
+
after = ["network.target" "pds.service"];
34
+
wantedBy = ["multi-user.target"];
35
+
36
+
serviceConfig = {
37
+
User = "jetstream";
38
+
Group = "jetstream";
39
+
StateDirectory = "jetstream";
40
+
StateDirectoryMode = "0755";
41
+
# preStart = ''
42
+
# mkdir -p "${cfg.dataDir}"
43
+
# chown -R jetstream:jetstream "${cfg.dataDir}"
44
+
# '';
45
+
# WorkingDirectory = cfg.dataDir;
46
+
Environment = [
47
+
"JETSTREAM_DATA_DIR=/var/lib/jetstream/data"
48
+
"JETSTREAM_LIVENESS_TTL=${toString cfg.livenessTtl}s"
49
+
"JETSTREAM_WS_URL=${cfg.websocketUrl}"
50
+
];
51
+
ExecStart = getExe cfg.package;
52
+
Restart = "always";
53
+
RestartSec = 5;
54
+
};
55
+
};
56
+
users = {
57
+
users.jetstream = {
58
+
group = "jetstream";
59
+
isSystemUser = true;
60
+
};
61
+
groups.jetstream = {};
62
+
};
63
+
};
64
+
}
+48
nix/modules/bluesky-relay.nix
+48
nix/modules/bluesky-relay.nix
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.bluesky-relay;
8
+
in
9
+
with lib; {
10
+
options.services.bluesky-relay = {
11
+
enable = mkEnableOption "relay server";
12
+
package = mkPackageOption pkgs "bluesky-relay" {};
13
+
};
14
+
config = mkIf cfg.enable {
15
+
systemd.services.bluesky-relay = {
16
+
description = "bluesky relay";
17
+
after = ["network.target" "pds.service"];
18
+
wantedBy = ["multi-user.target"];
19
+
20
+
serviceConfig = {
21
+
User = "relay";
22
+
Group = "relay";
23
+
StateDirectory = "relay";
24
+
StateDirectoryMode = "0755";
25
+
Environment = [
26
+
"RELAY_ADMIN_PASSWORD=password"
27
+
"RELAY_PLC_HOST=https://plc.tngl.boltless.dev"
28
+
"DATABASE_URL=sqlite:///var/lib/relay/relay.sqlite"
29
+
"RELAY_IP_BIND=:2470"
30
+
"RELAY_PERSIST_DIR=/var/lib/relay"
31
+
"RELAY_DISABLE_REQUEST_CRAWL=0"
32
+
"RELAY_INITIAL_SEQ_NUMBER=1"
33
+
"RELAY_ALLOW_INSECURE_HOSTS=1"
34
+
];
35
+
ExecStart = "${getExe cfg.package} serve";
36
+
Restart = "always";
37
+
RestartSec = 5;
38
+
};
39
+
};
40
+
users = {
41
+
users.relay = {
42
+
group = "relay";
43
+
isSystemUser = true;
44
+
};
45
+
groups.relay = {};
46
+
};
47
+
};
48
+
}
+76
nix/modules/did-method-plc.nix
+76
nix/modules/did-method-plc.nix
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.did-method-plc;
8
+
in
9
+
with lib; {
10
+
options.services.did-method-plc = {
11
+
enable = mkEnableOption "did-method-plc server";
12
+
package = mkPackageOption pkgs "did-method-plc" {};
13
+
};
14
+
config = mkIf cfg.enable {
15
+
services.postgresql = {
16
+
enable = true;
17
+
package = pkgs.postgresql_14;
18
+
ensureDatabases = ["plc"];
19
+
ensureUsers = [
20
+
{
21
+
name = "pg";
22
+
# ensurePermissions."DATABASE plc" = "ALL PRIVILEGES";
23
+
}
24
+
];
25
+
authentication = ''
26
+
local all all trust
27
+
host all all 127.0.0.1/32 trust
28
+
'';
29
+
};
30
+
systemd.services.did-method-plc = {
31
+
description = "did-method-plc";
32
+
33
+
after = ["postgresql.service"];
34
+
wants = ["postgresql.service"];
35
+
wantedBy = ["multi-user.target"];
36
+
37
+
environment = let
38
+
db_creds_json = builtins.toJSON {
39
+
username = "pg";
40
+
password = "";
41
+
host = "127.0.0.1";
42
+
port = 5432;
43
+
};
44
+
in {
45
+
# TODO: inherit from config
46
+
DEBUG_MODE = "1";
47
+
LOG_ENABLED = "true";
48
+
LOG_LEVEL = "debug";
49
+
LOG_DESTINATION = "1";
50
+
ENABLE_MIGRATIONS = "true";
51
+
DB_CREDS_JSON = db_creds_json;
52
+
DB_MIGRATE_CREDS_JSON = db_creds_json;
53
+
PLC_VERSION = "0.0.1";
54
+
PORT = "8080";
55
+
};
56
+
57
+
serviceConfig = {
58
+
ExecStart = getExe cfg.package;
59
+
User = "plc";
60
+
Group = "plc";
61
+
StateDirectory = "plc";
62
+
StateDirectoryMode = "0755";
63
+
Restart = "always";
64
+
65
+
# Hardening
66
+
};
67
+
};
68
+
users = {
69
+
users.plc = {
70
+
group = "plc";
71
+
isSystemUser = true;
72
+
};
73
+
groups.plc = {};
74
+
};
75
+
};
76
+
}
+20
nix/pkgs/bluesky-jetstream.nix
+20
nix/pkgs/bluesky-jetstream.nix
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "bluesky-jetstream";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "bluesky-social";
10
+
repo = "jetstream";
11
+
rev = "7d7efa58d7f14101a80ccc4f1085953948b7d5de";
12
+
sha256 = "sha256-1e9SL/8gaDPMA4YZed51ffzgpkptbMd0VTbTTDbPTFw=";
13
+
};
14
+
subPackages = ["cmd/jetstream"];
15
+
vendorHash = "sha256-/21XJQH6fo9uPzlABUAbdBwt1O90odmppH6gXu2wkiQ=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "jetstream";
19
+
};
20
+
}
+20
nix/pkgs/bluesky-relay.nix
+20
nix/pkgs/bluesky-relay.nix
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "bluesky-relay";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "boltlessengineer";
10
+
repo = "indigo";
11
+
rev = "7fe70a304d795b998f354d2b7b2050b909709c99";
12
+
sha256 = "sha256-+h34x67cqH5t30+8rua53/ucvbn3BanrmH0Og3moHok=";
13
+
};
14
+
subPackages = ["cmd/relay"];
15
+
vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "relay";
19
+
};
20
+
}
+65
nix/pkgs/did-method-plc.nix
+65
nix/pkgs/did-method-plc.nix
···
1
+
# inspired by https://github.com/NixOS/nixpkgs/blob/333bfb7c258fab089a834555ea1c435674c459b4/pkgs/by-name/ga/gatsby-cli/package.nix
2
+
{
3
+
lib,
4
+
stdenv,
5
+
fetchFromGitHub,
6
+
fetchYarnDeps,
7
+
yarnConfigHook,
8
+
yarnBuildHook,
9
+
nodejs,
10
+
makeBinaryWrapper,
11
+
}:
12
+
stdenv.mkDerivation (finalAttrs: {
13
+
pname = "did-method-plc";
14
+
version = "0.0.1";
15
+
16
+
src = fetchFromGitHub {
17
+
owner = "did-method-plc";
18
+
repo = "did-method-plc";
19
+
rev = "158ba5535ac3da4fd4309954bde41deab0b45972";
20
+
sha256 = "sha256-O5smubbrnTDMCvL6iRyMXkddr5G7YHxkQRVMRULHanQ=";
21
+
};
22
+
postPatch = ''
23
+
# remove dd-trace dependency
24
+
sed -i '3d' packages/server/service/index.js
25
+
'';
26
+
27
+
yarnOfflineCache = fetchYarnDeps {
28
+
yarnLock = finalAttrs.src + "/yarn.lock";
29
+
hash = "sha256-g8GzaAbWSnWwbQjJMV2DL5/ZlWCCX0sRkjjvX3tqU4Y=";
30
+
};
31
+
32
+
nativeBuildInputs = [
33
+
yarnConfigHook
34
+
yarnBuildHook
35
+
nodejs
36
+
makeBinaryWrapper
37
+
];
38
+
yarnBuildScript = "lerna";
39
+
yarnBuildFlags = [
40
+
"run"
41
+
"build"
42
+
"--scope"
43
+
"@did-plc/server"
44
+
"--include-dependencies"
45
+
];
46
+
47
+
installPhase = ''
48
+
runHook preInstall
49
+
50
+
mkdir -p $out/lib/node_modules/
51
+
mv packages/ $out/lib/packages/
52
+
mv node_modules/* $out/lib/node_modules/
53
+
54
+
makeWrapper ${lib.getExe nodejs} $out/bin/plc \
55
+
--add-flags $out/lib/packages/server/service/index.js \
56
+
--add-flags --enable-source-maps \
57
+
--set NODE_PATH $out/lib/node_modules
58
+
59
+
runHook postInstall
60
+
'';
61
+
62
+
meta = {
63
+
mainProgram = "plc";
64
+
};
65
+
})
+20
nix/pkgs/tap.nix
+20
nix/pkgs/tap.nix
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "tap";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "bluesky-social";
10
+
repo = "indigo";
11
+
rev = "498ecb9693e8ae050f73234c86f340f51ad896a9";
12
+
sha256 = "sha256-KASCdwkg/hlKBt7RTW3e3R5J3hqJkphoarFbaMgtN1k=";
13
+
};
14
+
subPackages = ["cmd/tap"];
15
+
vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "tap";
19
+
};
20
+
}
+122
nix/vm.nix
+122
nix/vm.nix
···
23
23
nixpkgs.lib.nixosSystem {
24
24
inherit system;
25
25
modules = [
26
+
self.nixosModules.did-method-plc
27
+
self.nixosModules.bluesky-jetstream
28
+
self.nixosModules.bluesky-relay
26
29
self.nixosModules.knot
27
30
self.nixosModules.spindle
28
31
({
···
39
42
diskSize = 10 * 1024;
40
43
cores = 2;
41
44
forwardPorts = [
45
+
# caddy
46
+
{
47
+
from = "host";
48
+
host.port = 80;
49
+
guest.port = 80;
50
+
}
51
+
{
52
+
from = "host";
53
+
host.port = 443;
54
+
guest.port = 443;
55
+
}
56
+
{
57
+
from = "host";
58
+
proto = "udp";
59
+
host.port = 443;
60
+
guest.port = 443;
61
+
}
42
62
# ssh
43
63
{
44
64
from = "host";
···
63
83
# as SQLite is incompatible with them. So instead we
64
84
# mount the shared directories to a different location
65
85
# and copy the contents around on service start/stop.
86
+
caddyData = {
87
+
source = "$TANGLED_VM_DATA_DIR/caddy";
88
+
target = config.services.caddy.dataDir;
89
+
};
66
90
knotData = {
67
91
source = "$TANGLED_VM_DATA_DIR/knot";
68
92
target = "/mnt/knot-data";
···
79
103
};
80
104
# This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall
81
105
networking.firewall.enable = false;
106
+
# resolve `*.tngl.boltless.dev` to host
107
+
services.dnsmasq.enable = true;
108
+
services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2";
109
+
security.pki.certificates = [
110
+
(builtins.readFile ../contrib/certs/root.crt)
111
+
];
82
112
time.timeZone = "Europe/London";
113
+
services.timesyncd.enable = lib.mkVMOverride true;
83
114
services.getty.autologinUser = "root";
84
115
environment.systemPackages = with pkgs; [curl vim git sqlite litecli];
116
+
virtualisation.docker.extraOptions = ''
117
+
--dns 172.17.0.1
118
+
'';
85
119
services.tangled.knot = {
86
120
enable = true;
87
121
motd = "Welcome to the development knot!\n";
···
108
142
provider = "sqlite";
109
143
};
110
144
};
145
+
};
146
+
services.did-method-plc.enable = true;
147
+
services.bluesky-pds = {
148
+
enable = true;
149
+
# overriding package version to support emails
150
+
package = pkgs.bluesky-pds.overrideAttrs (old: rec {
151
+
version = "0.4.188";
152
+
src = pkgs.fetchFromGitHub {
153
+
owner = "bluesky-social";
154
+
repo = "pds";
155
+
tag = "v${version}";
156
+
hash = "sha256-t8KdyEygXdbj/5Rhj8W40e1o8mXprELpjsKddHExmo0=";
157
+
};
158
+
pnpmDeps = pkgs.fetchPnpmDeps {
159
+
inherit version src;
160
+
pname = old.pname;
161
+
sourceRoot = old.sourceRoot;
162
+
fetcherVersion = 2;
163
+
hash = "sha256-lQie7f8JbWKSpoavnMjHegBzH3GB9teXsn+S2SLJHHU=";
164
+
};
165
+
});
166
+
settings = {
167
+
LOG_ENABLED = "true";
168
+
169
+
PDS_JWT_SECRET = "8cae8bffcc73d9932819650791e4e89a";
170
+
PDS_ADMIN_PASSWORD = "d6a902588cd93bee1af83f924f60cfd3";
171
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = "2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7";
172
+
173
+
PDS_EMAIL_SMTP_URL = envVarOr "TANGLED_VM_PDS_EMAIL_SMTP_URL" null;
174
+
PDS_EMAIL_FROM_ADDRESS = envVarOr "TANGLED_VM_PDS_EMAIL_FROM_ADDRESS" null;
175
+
176
+
PDS_DID_PLC_URL = "http://localhost:8080";
177
+
PDS_CRAWLERS = "https://relay.tngl.boltless.dev";
178
+
PDS_HOSTNAME = "pds.tngl.boltless.dev";
179
+
PDS_PORT = 3000;
180
+
};
181
+
};
182
+
services.bluesky-relay = {
183
+
enable = true;
184
+
};
185
+
services.bluesky-jetstream = {
186
+
enable = true;
187
+
livenessTtl = 300;
188
+
websocketUrl = "ws://localhost:3000/xrpc/com.atproto.sync.subscribeRepos";
189
+
};
190
+
services.caddy = {
191
+
enable = true;
192
+
configFile = pkgs.writeText "Caddyfile" ''
193
+
{
194
+
debug
195
+
cert_lifetime 3601d
196
+
pki {
197
+
ca local {
198
+
intermediate_lifetime 3599d
199
+
}
200
+
}
201
+
}
202
+
203
+
plc.tngl.boltless.dev {
204
+
tls internal
205
+
reverse_proxy http://localhost:8080
206
+
}
207
+
208
+
*.pds.tngl.boltless.dev, pds.tngl.boltless.dev {
209
+
tls internal
210
+
reverse_proxy http://localhost:3000
211
+
}
212
+
213
+
jetstream.tngl.boltless.dev {
214
+
tls internal
215
+
reverse_proxy http://localhost:6008
216
+
}
217
+
218
+
relay.tngl.boltless.dev {
219
+
tls internal
220
+
reverse_proxy http://localhost:2470
221
+
}
222
+
223
+
knot.tngl.boltless.dev {
224
+
tls internal
225
+
reverse_proxy http://localhost:6444
226
+
}
227
+
228
+
spindle.tngl.boltless.dev {
229
+
tls internal
230
+
reverse_proxy http://localhost:6555
231
+
}
232
+
'';
111
233
};
112
234
users = {
113
235
# So we don't have to deal with permission clashing between