wip: Custom mirroring tangled knot written in Rust
rust tangled mirror knot
1

Configure Feed

Select the types of activity you want to include in your feed.

app: add simple mirror button

backend not there yet but xrpc works!

Clément (Jun 9, 2026, 8:30 PM +0200) 65aba1e1 7ca01b66

+180 -1
+2
app/src/App.tsx
··· 3 3 import { SessionProvider } from "./session"; 4 4 import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; 5 5 import { RepositoriesList } from "./RepositoriesList"; 6 + import { MirrorButton } from "./MirrorButton"; 6 7 7 8 const queryClient = new QueryClient(); 8 9 ··· 21 22 </header> 22 23 <main> 23 24 <SignInButton /> 25 + <MirrorButton /> 24 26 <RepositoriesList /> 25 27 </main> 26 28 </div>
+149
app/src/MirrorButton.tsx
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + import { DevDrawbuKnottyCreate } from "./generated/lexicons"; 4 + import * as v from "valibot"; 5 + 6 + import { createForm } from "@tanstack/solid-form"; 7 + import { Button } from "./components/Button"; 8 + import { Dialog } from "./components/Dialog"; 9 + import { FormError } from "./components/FormError"; 10 + import { useSession } from "./session"; 11 + import { jwtHeaders } from "./lib/atproto"; 12 + 13 + async function createKnottyMirror( 14 + agent: OAuthUserAgent, 15 + params: DevDrawbuKnottyCreate.$input, 16 + ) { 17 + const rpc = new Client({ 18 + handler: simpleFetchHandler({ service: "https://knotty-test.drawbu.dev" }), 19 + }); 20 + const res = await rpc.post("dev.drawbu.knotty.create", { 21 + input: params, 22 + ...(await jwtHeaders(agent, "dev.drawbu.knotty.create")), 23 + }); 24 + if (!res.ok) return null; 25 + return res.data; 26 + } 27 + 28 + const MirrorNameSchema = v.pipe( 29 + v.string(), 30 + v.minLength(3, "Name is required."), 31 + v.regex( 32 + /^[a-zA-Z0-9-]+$/, 33 + "Name can only contain letters, numbers, or dashes.", 34 + ), 35 + ); 36 + 37 + export function MirrorButton() { 38 + const { agent } = useSession(); 39 + 40 + const form = createForm(() => ({ 41 + validators: { 42 + onSubmit: v.object({ 43 + url: v.pipe(v.string(), v.url()), 44 + name: MirrorNameSchema, 45 + }), 46 + }, 47 + defaultValues: { url: "", name: "" }, 48 + onSubmit: async ({ value }) => { 49 + const a = agent(); 50 + if (!a) return; 51 + await createKnottyMirror(a, { 52 + name: value.name, 53 + rkey: value.name, 54 + source: value.url, 55 + defaultBranch: undefined, 56 + }); 57 + }, 58 + })); 59 + 60 + let initialFocusInputRef: HTMLInputElement | undefined; 61 + 62 + return ( 63 + <Dialog 64 + title="create a mirror" 65 + description={<p>mirror a repository to your tangled account</p>} 66 + trigger={(p) => ( 67 + <Button 68 + variant="none" 69 + class="hover:text-green-300 hover:transition-colors" 70 + {...p} 71 + > 72 + mirror a new repo 73 + </Button> 74 + )} 75 + initialFocusEl={() => initialFocusInputRef!} 76 + > 77 + <form 78 + class="w-full max-w-[400px]" 79 + onSubmit={(e) => { 80 + e.preventDefault(); 81 + e.stopPropagation(); 82 + form.handleSubmit(); 83 + }} 84 + > 85 + <div class="flex flex-col gap-2"> 86 + <form.Field 87 + name="url" 88 + children={(field) => ( 89 + <label class="flex flex-col"> 90 + <span class="select-none">url:</span> 91 + <input 92 + ref={(el) => (initialFocusInputRef = el)} 93 + value={field().state.value} 94 + onBlur={field().handleBlur} 95 + onInput={(e) => field().handleChange(e.target.value)} 96 + placeholder="https://github.com/drawbu/knotty" 97 + class="border border-border-input bg-bg-input rounded-input px-3 py-2 focus-ring" 98 + aria-describedby="url-error" 99 + /> 100 + <FormError id="url-error" field={field()} /> 101 + </label> 102 + )} 103 + /> 104 + <form.Field 105 + name="name" 106 + children={(field) => ( 107 + <label class="flex flex-col"> 108 + <span class="select-none">name:</span> 109 + <span class="select-none aria-hidden"> 110 + created as:{" "} 111 + <code class="text-sm bg-zinc-800 px-2 py-1"> 112 + tangled.org/drawbu.dev/{field().state.value} 113 + </code> 114 + </span> 115 + <input 116 + value={field().state.value} 117 + onBlur={field().handleBlur} 118 + onInput={(e) => field().handleChange(e.target.value)} 119 + placeholder="knotty" 120 + class="border border-border-input bg-bg-input rounded-input px-3 py-2 focus-ring" 121 + aria-describedby="name-error" 122 + /> 123 + <FormError id="name-error" field={field()} /> 124 + </label> 125 + )} 126 + /> 127 + <div class="flex justify-end"> 128 + <form.Subscribe 129 + selector={({ canSubmit, isSubmitting }) => ({ 130 + canSubmit, 131 + isSubmitting, 132 + })} 133 + > 134 + {(p) => ( 135 + <Button 136 + variant="contrast" 137 + type="submit" 138 + disabled={!p().canSubmit || p().isSubmitting} 139 + > 140 + submit 141 + </Button> 142 + )} 143 + </form.Subscribe> 144 + </div> 145 + </div> 146 + </form> 147 + </Dialog> 148 + ); 149 + }
+28
app/src/lib/atproto.ts
··· 1 + import { XRPCProcedures } from "@atcute/lexicons/ambient"; 2 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + 4 + const KNOTTY_DID = `did:web:${import.meta.env.KNOT_HOSTNAME}`; 5 + 6 + export async function getServiceAuthToken<TName extends keyof XRPCProcedures>( 7 + agent: OAuthUserAgent, 8 + lxm: TName, 9 + ): Promise<string> { 10 + const params = new URLSearchParams({ aud: KNOTTY_DID, lxm }); 11 + const res = await agent.handle( 12 + `/xrpc/com.atproto.server.getServiceAuth?${params}`, 13 + {}, 14 + ); 15 + if (!res.ok) throw new Error(`getServiceAuth failed: ${res.status}`); 16 + const data = (await res.json()) as { token: string }; 17 + return data.token; 18 + } 19 + 20 + export async function jwtHeaders<TName extends keyof XRPCProcedures>( 21 + agent: OAuthUserAgent, 22 + lxm: TName, 23 + ) { 24 + const token = await getServiceAuthToken(agent, lxm); 25 + return { 26 + headers: { Authorization: `Bearer ${token}` }, 27 + }; 28 + }
+1 -1
app/src/lib/oauth_metadata.ts
··· 1 1 import { Did } from "@atcute/lexicons"; 2 2 3 3 export function buildOAuthClientMetadata(hostname: string, isDev: boolean) { 4 - const scope = `atproto repo:sh.tangled.repo`; 4 + const scope = `atproto repo:sh.tangled.repo rpc:dev.drawbu.knotty.create?aud=*`; 5 5 6 6 const client_id = isDev 7 7 ? `http://localhost?redirect_uri=${encodeURIComponent(hostname)}&scope=${encodeURIComponent(scope)}`