Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

+4 -7
appview/pages/pages.go
··· 612 } 613 614 type FollowFragmentParams struct { 615 - UserDid string 616 - FollowStatus models.FollowStatus 617 - FollowersCount int64 618 } 619 620 func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 621 - return p.executePlain("user/fragments/follow-oob", w, params) 622 } 623 624 type EditBioParams struct { ··· 649 IsStarred bool 650 SubjectAt syntax.ATURI 651 StarCount int 652 - HxSwapOob bool 653 } 654 655 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 656 - params.HxSwapOob = true 657 - return p.executePlain("fragments/starBtn", w, params) 658 } 659 660 type RepoIndexParams struct {
··· 612 } 613 614 type FollowFragmentParams struct { 615 + UserDid string 616 + FollowStatus models.FollowStatus 617 } 618 619 func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 620 + return p.executePlain("user/fragments/follow", w, params) 621 } 622 623 type EditBioParams struct { ··· 648 IsStarred bool 649 SubjectAt syntax.ATURI 650 StarCount int 651 } 652 653 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 654 + return p.executePlain("fragments/starBtn-oob", w, params) 655 } 656 657 type RepoIndexParams struct {
+5
appview/pages/templates/fragments/starBtn-oob.html
···
··· 1 + {{ define "fragments/starBtn-oob" }} 2 + <div hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'> 3 + {{ template "fragments/starBtn" . }} 4 + </div> 5 + {{ end }}
-1
appview/pages/templates/fragments/starBtn.html
··· 9 {{ else }} 10 hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 11 {{ end }} 12 - {{ if .HxSwapOob }}hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'{{ end }} 13 14 hx-trigger="click" 15 hx-disabled-elt="#starBtn"
··· 9 {{ else }} 10 hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 11 {{ end }} 12 13 hx-trigger="click" 14 hx-disabled-elt="#starBtn"
+2 -2
appview/pages/templates/layouts/fragments/footer.html
··· 47 48 <!-- Right section --> 49 <div class="text-right"> 50 - <div class="text-xs">&copy; 2026 Tangled Labs Oy. All rights reserved.</div> 51 </div> 52 </div> 53 ··· 93 </div> 94 95 <div class="text-center"> 96 - <div class="text-xs">&copy; 2026 Tangled Labs Oy. All rights reserved.</div> 97 </div> 98 </div> 99 </div>
··· 47 48 <!-- Right section --> 49 <div class="text-right"> 50 + <div class="text-xs">&copy; 2025 Tangled Labs Oy. All rights reserved.</div> 51 </div> 52 </div> 53 ··· 93 </div> 94 95 <div class="text-center"> 96 + <div class="text-xs">&copy; 2025 Tangled Labs Oy. All rights reserved.</div> 97 </div> 98 </div> 99 </div>
-6
appview/pages/templates/user/fragments/follow-oob.html
··· 1 - {{ define "user/fragments/follow-oob" }} 2 - {{ template "user/fragments/follow" . }} 3 - <span hx-swap-oob='innerHTML:[data-followers-did="{{ .UserDid }}"]'> 4 - <a href="/{{ resolve .UserDid }}?tab=followers">{{ .FollowersCount }} followers</a> 5 - </span> 6 - {{ end }}
···
+3 -5
appview/pages/templates/user/fragments/followCard.html
··· 9 <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0"> 10 <div class="flex-1 min-h-0 justify-around flex flex-col"> 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> 14 </a> 15 {{ with .Profile }} 16 <p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p> 17 {{ end }} 18 <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 19 <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> 22 <span class="select-none after:content-['ยท']"></span> 23 <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 24 </div> ··· 31 </div> 32 </div> 33 </div> 34 - {{ end }}
··· 9 <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0"> 10 <div class="flex-1 min-h-0 justify-around flex flex-col"> 11 <a href="/{{ $userIdent }}"> 12 + <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span> 13 </a> 14 {{ with .Profile }} 15 <p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p> 16 {{ end }} 17 <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 18 <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 19 + <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 20 <span class="select-none after:content-['ยท']"></span> 21 <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 22 </div> ··· 29 </div> 30 </div> 31 </div> 32 + {{ end }}
+99 -97
appview/pages/templates/user/fragments/profileCard.html
··· 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> 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 }} 30 31 - {{ if .Description }} 32 - <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 33 - {{ end }} 34 35 - <div class="hidden md:block"> 36 - {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 37 - </div> 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> 68 </div> 69 {{ end }} 70 - {{ end }} 71 - </div> 72 - {{ end }} 73 - </div> 74 - {{ end }} 75 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 }} 87 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> 93 94 </div> 95 - <div id="update-profile" class="text-red-400 dark:text-red-500"></div> 96 - </div> 97 - </div> 98 {{ end }} 99 100 {{ 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> 111 {{ end }} 112 - {{ end }}
··· 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> 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 }} 30 31 + {{ if .Description }} 32 + <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 33 + {{ end }} 34 35 + <div class="hidden md:block"> 36 + {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 37 + </div> 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" }}</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 }} 72 </div> 73 {{ end }} 74 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 }} 89 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> 95 96 + </div> 97 + <div id="update-profile" class="text-red-400 dark:text-red-500"></div> 98 + </div> 99 </div> 100 {{ end }} 101 102 {{ define "followerFollowing" }} 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 }} 113 {{ end }} 114 +
+5 -16
appview/pulls/pulls.go
··· 1 package pulls 2 3 import ( 4 - "bytes" 5 - "compress/gzip" 6 "context" 7 "database/sql" 8 "encoding/json" 9 "errors" 10 "fmt" 11 - "io" 12 "log" 13 "log/slog" 14 "net/http" ··· 1244 return 1245 } 1246 1247 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 1248 if err != nil { 1249 log.Println("failed to upload patch", err) 1250 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1338 // apply all record creations at once 1339 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1340 for _, p := range stack { 1341 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch())) 1342 if err != nil { 1343 log.Println("failed to upload patch blob", err) 1344 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1888 return 1889 } 1890 1891 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 1892 if err != nil { 1893 log.Println("failed to upload patch blob", err) 1894 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2030 return 2031 } 2032 2033 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 2034 if err != nil { 2035 log.Println("failed to upload patch blob", err) 2036 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2072 return 2073 } 2074 2075 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 2076 if err != nil { 2077 log.Println("failed to upload patch blob", err) 2078 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2455 2456 return stack, nil 2457 } 2458 - 2459 - func gz(s string) io.Reader { 2460 - var b bytes.Buffer 2461 - w := gzip.NewWriter(&b) 2462 - w.Write([]byte(s)) 2463 - w.Close() 2464 - return &b 2465 - }
··· 1 package pulls 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "log" 10 "log/slog" 11 "net/http" ··· 1241 return 1242 } 1243 1244 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1245 if err != nil { 1246 log.Println("failed to upload patch", err) 1247 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1335 // apply all record creations at once 1336 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1337 for _, p := range stack { 1338 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1339 if err != nil { 1340 log.Println("failed to upload patch blob", err) 1341 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1885 return 1886 } 1887 1888 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1889 if err != nil { 1890 log.Println("failed to upload patch blob", err) 1891 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2027 return 2028 } 2029 2030 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2031 if err != nil { 2032 log.Println("failed to upload patch blob", err) 2033 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2069 return 2070 } 2071 2072 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2073 if err != nil { 2074 log.Println("failed to upload patch blob", err) 2075 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2452 2453 return stack, nil 2454 }
+4 -16
appview/state/follow.go
··· 75 76 s.notifier.NewFollow(r.Context(), follow) 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 s.pages.FollowFragment(w, pages.FollowFragmentParams{ 84 - UserDid: subjectIdent.DID.String(), 85 - FollowStatus: models.IsFollowing, 86 - FollowersCount: followStats.Followers, 87 }) 88 89 return ··· 112 // this is not an issue, the firehose event might have already done this 113 } 114 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 s.pages.FollowFragment(w, pages.FollowFragmentParams{ 121 - UserDid: subjectIdent.DID.String(), 122 - FollowStatus: models.IsNotFollowing, 123 - FollowersCount: followStats.Followers, 124 }) 125 126 s.notifier.DeleteFollow(r.Context(), follow)
··· 75 76 s.notifier.NewFollow(r.Context(), follow) 77 78 s.pages.FollowFragment(w, pages.FollowFragmentParams{ 79 + UserDid: subjectIdent.DID.String(), 80 + FollowStatus: models.IsFollowing, 81 }) 82 83 return ··· 106 // this is not an issue, the firehose event might have already done this 107 } 108 109 s.pages.FollowFragment(w, pages.FollowFragmentParams{ 110 + UserDid: subjectIdent.DID.String(), 111 + FollowStatus: models.IsNotFollowing, 112 }) 113 114 s.notifier.DeleteFollow(r.Context(), follow)
+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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
··· 663 nixpkgs: 664 - nodejs 665 - go 666 - # unstable 667 - nixpkgs/nixpkgs-unstable: 668 - - bun 669 # custom registry 670 git+https://tangled.org/@example.com/my_pkg: 671 - my_pkg
··· 663 nixpkgs: 664 - nodejs 665 - go 666 # custom registry 667 git+https://tangled.org/@example.com/my_pkg: 668 - my_pkg
+34 -2
flake.nix
··· 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 98 }); 99 in { 100 overlays.default = final: prev: { 101 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 102 }; 103 104 packages = forAllSystems (system: let ··· 119 sqlite-lib 120 docs 121 dolly 122 ; 123 124 pkgsStatic-appview = staticPackages.appview; ··· 248 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 249 cd "$rootDir" 250 251 - mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs} 252 253 export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 254 exec ${pkgs.lib.getExe ··· 320 imports = [./nix/modules/spindle.nix]; 321 322 services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle; 323 }; 324 }; 325 }
··· 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 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 {}; 102 }); 103 in { 104 overlays.default = final: prev: { 105 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly did-method-plc bluesky-jetstream bluesky-relay tap; 106 }; 107 108 packages = forAllSystems (system: let ··· 123 sqlite-lib 124 docs 125 dolly 126 + did-method-plc 127 + bluesky-jetstream 128 + bluesky-relay 129 + tap 130 ; 131 132 pkgsStatic-appview = staticPackages.appview; ··· 256 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 257 cd "$rootDir" 258 259 + mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs} 260 261 export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 262 exec ${pkgs.lib.getExe ··· 328 imports = [./nix/modules/spindle.nix]; 329 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; 355 }; 356 }; 357 }
-4
input.css
··· 133 disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 134 } 135 136 - .prose { 137 - overflow-wrap: anywhere; 138 - } 139 - 140 .prose hr { 141 @apply my-2; 142 }
··· 133 disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 134 } 135 136 .prose hr { 137 @apply my-2; 138 }
-3
nix/modules/appview.nix
··· 1 { 2 - pkgs, 3 config, 4 lib, 5 ... ··· 260 after = ["redis-appview.service" "network-online.target"]; 261 requires = ["redis-appview.service"]; 262 wants = ["network-online.target"]; 263 - 264 - path = [pkgs.diffutils]; 265 266 serviceConfig = { 267 Type = "simple";
··· 1 { 2 config, 3 lib, 4 ... ··· 259 after = ["redis-appview.service" "network-online.target"]; 260 requires = ["redis-appview.service"]; 261 wants = ["network-online.target"]; 262 263 serviceConfig = { 264 Type = "simple";
+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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
··· 23 nixpkgs.lib.nixosSystem { 24 inherit system; 25 modules = [ 26 self.nixosModules.knot 27 self.nixosModules.spindle 28 ({ ··· 39 diskSize = 10 * 1024; 40 cores = 2; 41 forwardPorts = [ 42 # ssh 43 { 44 from = "host"; ··· 63 # as SQLite is incompatible with them. So instead we 64 # mount the shared directories to a different location 65 # and copy the contents around on service start/stop. 66 knotData = { 67 source = "$TANGLED_VM_DATA_DIR/knot"; 68 target = "/mnt/knot-data"; ··· 79 }; 80 # 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 networking.firewall.enable = false; 82 time.timeZone = "Europe/London"; 83 services.getty.autologinUser = "root"; 84 environment.systemPackages = with pkgs; [curl vim git sqlite litecli]; 85 services.tangled.knot = { 86 enable = true; 87 motd = "Welcome to the development knot!\n"; ··· 108 provider = "sqlite"; 109 }; 110 }; 111 }; 112 users = { 113 # So we don't have to deal with permission clashing between
··· 23 nixpkgs.lib.nixosSystem { 24 inherit system; 25 modules = [ 26 + self.nixosModules.did-method-plc 27 + self.nixosModules.bluesky-jetstream 28 + self.nixosModules.bluesky-relay 29 self.nixosModules.knot 30 self.nixosModules.spindle 31 ({ ··· 42 diskSize = 10 * 1024; 43 cores = 2; 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 + } 62 # ssh 63 { 64 from = "host"; ··· 83 # as SQLite is incompatible with them. So instead we 84 # mount the shared directories to a different location 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 + }; 90 knotData = { 91 source = "$TANGLED_VM_DATA_DIR/knot"; 92 target = "/mnt/knot-data"; ··· 103 }; 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 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 + ]; 112 time.timeZone = "Europe/London"; 113 + services.timesyncd.enable = lib.mkVMOverride true; 114 services.getty.autologinUser = "root"; 115 environment.systemPackages = with pkgs; [curl vim git sqlite litecli]; 116 + virtualisation.docker.extraOptions = '' 117 + --dns 172.17.0.1 118 + ''; 119 services.tangled.knot = { 120 enable = true; 121 motd = "Welcome to the development knot!\n"; ··· 142 provider = "sqlite"; 143 }; 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 + ''; 233 }; 234 users = { 235 # So we don't have to deal with permission clashing between