view who was fronting when a record was made

feat: handle most kinds of posts and views in social-app, cache fronters, etc

ptr.pet 96c974d5 996cbea0

verified
Changed files
+393 -92
src
+184 -24
src/entrypoints/background.ts
··· 1 - import { getFronter, putFronter } from "@/lib/utils"; 1 + import { expect } from "@/lib/result"; 2 + import { 3 + Fronter, 4 + fronterGetSocialAppHref, 5 + getFronter, 6 + putFronter, 7 + } from "@/lib/utils"; 8 + import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; 2 9 3 10 export default defineBackground({ 4 11 persistent: true, 5 12 main: () => { 6 13 console.log("setting up background script"); 7 - browser.runtime.onMessage.addListener(async (message, sender) => { 8 - // console.log("received message", message); 9 - if (message.type !== "RESPONSE_CAPTURED") return; 10 - const fronter = await storage.getItem<string>("sync:fronter"); 11 - if (!fronter) return; 12 - const authToken = message.data.authToken; 13 - if (!authToken) return; 14 - const data: any = JSON.parse(message.data.body); 15 - // console.log("will put fronter", fronter, "for records", data.results); 16 - for (const result of data.results) { 17 - // TODO: validate response 18 - await putFronter(result.uri, fronter, authToken); 19 - } 20 - }); 21 14 22 - browser.runtime.onMessage.addListener(async (message, sender) => { 23 - // console.log("received message", message); 24 - if (message.type !== "TAB_FRONTER") return; 25 - const { recordUri } = message; 26 - const fronter = await getFronter(recordUri); 27 - if (!fronter.ok) return; 15 + let fronters = new Map<ResourceUri, Fronter | null>(); 16 + const cacheFronter = (uri: ResourceUri, fronter: Fronter) => { 17 + const parsedUri = expect(parseResourceUri(uri)); 18 + fronters.set(uri, fronter); 19 + fronters.set( 20 + `at://${fronter.did}/${parsedUri.collection!}/${parsedUri.rkey!}`, 21 + fronter, 22 + ); 23 + fronters.set( 24 + `at://${fronter.handle}/${parsedUri.collection!}/${parsedUri.rkey!}`, 25 + fronter, 26 + ); 27 + return parsedUri; 28 + }; 29 + 30 + const setTabFronter = async (recordUri: ResourceUri, fronter: Fronter) => { 28 31 const tabs = await browser.tabs.query({ 29 32 active: true, 30 33 currentWindow: true, 31 34 }); 32 35 const tab = tabs[0]; 33 36 const tabKey: StorageItemKey = `local:tab-${tab.id!}-fronter`; 34 - await storage.setItem(tabKey, { 35 - fronterName: fronter.value.name, 37 + const tabFronter = { 36 38 recordUri, 37 - }); 39 + ...fronter, 40 + }; 41 + await storage.setItem(tabKey, tabFronter); 38 42 const deleteOld = async (tabId: number) => { 39 43 if (`local:tab-${tabId}-fronter` !== tabKey) return; 40 44 await storage.removeItem(tabKey); ··· 42 46 browser.tabs.onRemoved.addListener(deleteOld); 43 47 browser.tabs.onReplaced.addListener(deleteOld); 44 48 browser.tabs.onUpdated.addListener(deleteOld); 49 + }; 50 + 51 + const handleWrite = async ( 52 + { data: { body, authToken } }: any, 53 + sender: globalThis.Browser.runtime.MessageSender, 54 + ) => { 55 + const fronter = await storage.getItem<string>("sync:fronter"); 56 + if (!fronter) return; 57 + if (!authToken) return; 58 + const data: any = JSON.parse(body); 59 + // console.log("will put fronter", fronter, "for records", data.results); 60 + const results = []; 61 + for (const result of data.results) { 62 + const resp = await putFronter(result.uri, fronter, authToken); 63 + if (resp.ok) { 64 + const parsedUri = cacheFronter(result.uri, resp.value); 65 + results.push({ 66 + rkey: parsedUri.rkey!, 67 + ...resp.value, 68 + }); 69 + } 70 + } 71 + browser.tabs.sendMessage(sender.tab?.id!, { 72 + type: "TIMELINE_FRONTER", 73 + results: new Map( 74 + results.map((fronter) => [ 75 + fronterGetSocialAppHref(fronter, fronter.rkey), 76 + fronter, 77 + ]), 78 + ), 79 + }); 80 + }; 81 + const handleTimeline = async ( 82 + feed: any[], 83 + sender: globalThis.Browser.runtime.MessageSender, 84 + ) => { 85 + const handlePost = async (post: any) => { 86 + const cachedFronter = fronters.get(post.uri); 87 + if (cachedFronter === null) return; 88 + const promise = cachedFronter 89 + ? Promise.resolve(cachedFronter) 90 + : getFronter(post.uri).then(async (fronter) => { 91 + if (!fronter.ok) { 92 + fronters.set(post.uri, null); 93 + return; 94 + } 95 + return fronter.value; 96 + }); 97 + return promise.then((fronter) => { 98 + if (!fronter) return; 99 + const parsedUri = cacheFronter(post.uri, fronter); 100 + return { 101 + rkey: parsedUri.rkey!, 102 + ...fronter, 103 + }; 104 + }); 105 + }; 106 + const allPromises = feed.flatMap((item) => { 107 + const promises = [handlePost(item.post)]; 108 + if (item.reply?.parent) { 109 + promises.push(handlePost(item.reply.parent)); 110 + } 111 + if (item.reply?.root) { 112 + promises.push(handlePost(item.reply.root)); 113 + } 114 + return promises; 115 + }); 116 + const results = new Map( 117 + (await Promise.allSettled(allPromises)) 118 + .filter((result) => result.status === "fulfilled") 119 + .flatMap((result) => result.value ?? []) 120 + .map((fronter) => [ 121 + fronterGetSocialAppHref(fronter, fronter.rkey), 122 + fronter, 123 + ]), 124 + ); 125 + browser.tabs.sendMessage(sender.tab?.id!, { 126 + type: "TIMELINE_FRONTER", 127 + results, 128 + }); 129 + // console.log("sent timeline fronters", results); 130 + }; 131 + const handleThread = async ( 132 + { data: { body } }: any, 133 + sender: globalThis.Browser.runtime.MessageSender, 134 + ) => { 135 + const data: any = JSON.parse(body); 136 + const promises = (data.thread as any[]).flatMap((item) => { 137 + const cachedFronter = fronters.get(item.uri); 138 + if (cachedFronter === null) return []; 139 + const promise = cachedFronter 140 + ? Promise.resolve(cachedFronter) 141 + : getFronter(item.uri).then(async (fronter) => { 142 + if (!fronter.ok) { 143 + fronters.set(item.uri, null); 144 + return; 145 + } 146 + return fronter.value; 147 + }); 148 + return promise.then(async (fronter) => { 149 + if (!fronter) return; 150 + const parsedUri = cacheFronter(item.uri, fronter); 151 + if (item.depth === 0) await setTabFronter(item.uri, fronter); 152 + return { 153 + rkey: parsedUri.rkey!, 154 + displayName: item.value.post.author.displayName, 155 + depth: item.depth, 156 + ...fronter, 157 + }; 158 + }); 159 + }); 160 + const results = new Map( 161 + (await Promise.allSettled(promises)) 162 + .filter((result) => result.status === "fulfilled") 163 + .flatMap((result) => result.value ?? []) 164 + .map((fronter) => [ 165 + fronterGetSocialAppHref(fronter, fronter.rkey, fronter.depth), 166 + fronter, 167 + ]), 168 + ); 169 + browser.tabs.sendMessage(sender.tab?.id!, { 170 + type: "THREAD_FRONTER", 171 + results, 172 + }); 173 + // console.log("sent thread fronters", results); 174 + }; 175 + 176 + browser.runtime.onMessage.addListener(async (message, sender) => { 177 + if (message.type !== "RESPONSE_CAPTURED") return; 178 + // console.log("handling response event", message); 179 + switch (message.data.type as string) { 180 + case "write": 181 + await handleWrite(message, sender); 182 + break; 183 + case "posts": 184 + const posts = JSON.parse(message.data.body) as any[]; 185 + await handleTimeline( 186 + posts.map((post) => ({ post })), 187 + sender, 188 + ); 189 + break; 190 + case "timeline": 191 + await handleTimeline(JSON.parse(message.data.body).feed, sender); 192 + break; 193 + case "thread": 194 + await handleThread(message, sender); 195 + break; 196 + } 197 + browser.tabs.sendMessage(sender.tab?.id!, { 198 + type: "CACHED_FRONTERS", 199 + fronters, 200 + }); 201 + }); 202 + browser.runtime.onMessage.addListener(async (message, sender) => { 203 + if (message.type !== "TAB_FRONTER") return; 204 + await setTabFronter(message.recordUri, message.fronter); 45 205 }); 46 206 }, 47 207 });
+92 -29
src/entrypoints/content.ts
··· 1 + import { expect } from "@/lib/result"; 2 + import { Fronter, fronterGetSocialAppHref } from "@/lib/utils"; 3 + import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; 4 + 1 5 const getAuthHeader = (headers: any): string | null => { 2 6 if (headers instanceof Headers) { 3 7 return headers.get("authorization"); ··· 21 25 const response = await originalFetch.apply(this, args); 22 26 23 27 if (respEventName === null) return response; 28 + if (response.status !== 200) return response; 24 29 25 - let authHeader: string | null = null; 26 - if (typeof args[0] === "string") { 27 - if (args[1]?.headers) { 28 - authHeader = getAuthHeader(args[1].headers); 29 - } 30 - } else if (args[0] instanceof Request) { 31 - authHeader = getAuthHeader(args[0].headers); 32 - } 30 + const body = await response.clone().text(); 33 31 34 - if ( 35 - !response.url.includes("/xrpc/com.atproto.repo.applyWrites") || 36 - response.status !== 200 37 - ) 38 - return response; 32 + const sendEvent = (detail: any) => { 33 + // console.log("sending response event", detail); 34 + window.dispatchEvent.call( 35 + window, 36 + new window.CustomEvent(`${respEventName}-isolated`, { 37 + detail, 38 + }), 39 + ); 40 + }; 39 41 40 - const body = await response.clone().text(); 42 + let detail: any; 43 + if (response.url.includes("/xrpc/com.atproto.repo.applyWrites")) { 44 + let authHeader: string | null = null; 45 + if (typeof args[0] === "string") { 46 + if (args[1]?.headers) { 47 + authHeader = getAuthHeader(args[1].headers); 48 + } 49 + } else if (args[0] instanceof Request) { 50 + authHeader = getAuthHeader(args[0].headers); 51 + } 41 52 42 - const detail = { 43 - url: response.url, 44 - body, 45 - authToken: authHeader?.split(" ")[1] || null, 46 - }; 47 - window.dispatchEvent.call( 48 - window, 49 - new window.CustomEvent(`${respEventName}-isolated`, { 50 - detail, 51 - }), 52 - ); 53 + detail = { 54 + type: "write", 55 + body, 56 + authToken: authHeader?.split(" ")[1] || null, 57 + }; 58 + } else if ( 59 + response.url.includes("/xrpc/app.bsky.feed.getAuthorFeed") || 60 + response.url.includes("/xrpc/app.bsky.feed.getTimeline") || 61 + response.url.includes("/xrpc/app.bsky.feed.getFeed") 62 + ) { 63 + detail = { 64 + type: "timeline", 65 + body, 66 + }; 67 + } else if ( 68 + response.url.includes("/xrpc/app.bsky.unspecced.getPostThreadV2") 69 + ) { 70 + detail = { 71 + type: "thread", 72 + body, 73 + }; 74 + } else if (response.url.includes("/xrpc/app.bsky.feed.getPosts")) { 75 + detail = { 76 + type: "posts", 77 + body, 78 + }; 79 + } 80 + sendEvent(detail); 53 81 54 82 return response; 55 83 }; 56 84 globalThis.fetch = overriddenFetch; 57 85 (globalThis as any).oldFetch = originalFetch; 58 86 59 - console.log("waiting for response channel setup..."); 60 87 const respEventSetup = new Promise<string>((resolve) => { 61 88 document.addEventListener( 62 89 "at-fronter-channel-setup", ··· 67 94 { once: true, capture: true }, 68 95 ); 69 96 }); 70 - respEventSetup.then((name) => { 71 - console.log("set up response channel ", name); 72 - respEventName = name; 97 + respEventSetup.then((name) => (respEventName = name)); 98 + 99 + const applyFronterName = (el: Element, fronterName: string) => { 100 + if (el.getAttribute("data-fronter")) return; 101 + el.textContent += ` [f: ${fronterName}]`; 102 + el.setAttribute("data-fronter", fronterName); 103 + }; 104 + const applyFrontersToPage = (fronters: Map<string, any>) => { 105 + for (const el of document.getElementsByTagName("a")) { 106 + const path = `/${el.href.split("/").slice(3).join("/")}`; 107 + const fronter = fronters.get(path); 108 + if (!fronter) continue; 109 + const isFocusedPost = fronter.depth === 0; 110 + if (isFocusedPost && el.ariaLabel !== fronter.displayName) continue; 111 + const displayNameElement = isFocusedPost 112 + ? (el.firstElementChild?.firstElementChild?.firstElementChild 113 + ?.firstElementChild?.firstElementChild ?? null) 114 + : (el.parentElement?.firstElementChild?.firstElementChild 115 + ?.firstElementChild?.firstElementChild ?? null); 116 + if (!displayNameElement) continue; 117 + applyFronterName(displayNameElement, fronter.fronterName); 118 + } 119 + }; 120 + window.addEventListener("message", (event) => { 121 + if (event.data.type !== "CACHED_FRONTERS") return; 122 + const fronters = event.data.fronters as Map<string, Fronter | null>; 123 + const updated = new Map( 124 + fronters.entries().flatMap(([uri, fronter]) => { 125 + if (!fronter) return []; 126 + const rkey = expect(parseResourceUri(uri)).rkey!; 127 + return [[fronterGetSocialAppHref(fronter, rkey), fronter]]; 128 + }), 129 + ); 130 + applyFrontersToPage(updated); 131 + }); 132 + window.addEventListener("message", (event) => { 133 + if (!["TIMELINE_FRONTER", "THREAD_FRONTER"].includes(event.data.type)) 134 + return; 135 + applyFrontersToPage(event.data.results as Map<string, any>); 73 136 }); 74 137 }, 75 138 });
+50 -16
src/entrypoints/isolated.content.ts
··· 1 - import { expect } from "@/lib/result"; 2 - import { getFronter } from "@/lib/utils"; 1 + import { Fronter } from "@/lib/utils"; 3 2 import { ResourceUri } from "@atcute/lexicons"; 4 3 5 4 export default defineContentScript({ ··· 7 6 runAt: "document_start", 8 7 world: "ISOLATED", 9 8 main: (ctx) => { 9 + let fronters = new Map<ResourceUri, Fronter | null>(); 10 + 10 11 const checkFronter = (url: string) => { 11 12 // match https://*/profile/<actor_identifier>/post/<rkey> regex with named params to extract actor_identifier and rkey 12 13 const match = url.match( 13 14 /https:\/\/[^/]+\/profile\/([^/]+)\/post\/([^/]+)/, 14 15 ); 15 - if (match) { 16 - const [website, actorIdentifier, rkey] = match; 17 - const recordUri = 18 - `at://${actorIdentifier}/app.bsky.feed.post/${rkey}` as ResourceUri; 19 - browser.runtime.sendMessage({ 20 - type: "TAB_FRONTER", 21 - recordUri, 22 - }); 23 - } 16 + if (!match) return false; 17 + const [website, actorIdentifier, rkey] = match; 18 + const recordUri = 19 + `at://${actorIdentifier}/app.bsky.feed.post/${rkey}` as ResourceUri; 20 + const fronter = fronters.get(recordUri); 21 + if (!fronter) return false; 22 + browser.runtime.sendMessage({ 23 + type: "TAB_FRONTER", 24 + recordUri, 25 + fronter, 26 + }); 27 + return true; 24 28 }; 29 + 25 30 const respEventName = Math.random().toString(36).slice(2); 26 31 window.addEventListener(`${respEventName}-isolated`, async (event) => { 27 - // console.log("sending event to bg:", event); 32 + const data = (event as any).detail; 33 + // console.log("passing response event to bg", data); 28 34 await browser.runtime 29 35 .sendMessage({ 30 36 type: "RESPONSE_CAPTURED", 31 - data: (event as any).detail, 37 + data, 32 38 }) 33 39 .catch(() => { 34 40 console.log("background script not ready"); 35 41 }); 36 42 }); 37 - ctx.addEventListener(window, "wxt:locationchange", async (event) => { 43 + const messageTypes = [ 44 + "TAB_FRONTER", 45 + "THREAD_FRONTER", 46 + "TIMELINE_FRONTER", 47 + "CACHED_FRONTERS", 48 + ]; 49 + browser.runtime.onMessage.addListener((message) => { 50 + if (!messageTypes.includes(message.type)) return; 51 + if (message.type === "CACHED_FRONTERS") { 52 + fronters = message.fronters; 53 + } 54 + window.postMessage(message); 55 + }); 56 + let postTabObserver: MutationObserver | null = null; 57 + ctx.addEventListener(window, "wxt:locationchange", (event) => { 58 + window.postMessage({ type: "CACHED_FRONTERS", fronters }); 59 + // check if we are on profile so we can update fronters if the post tab is clicked on 60 + const postTabElement = document.querySelector( 61 + '[data-testid="profilePager-Posts"]', 62 + ); 63 + if (postTabElement) { 64 + postTabObserver = new MutationObserver(() => { 65 + window.postMessage({ type: "CACHED_FRONTERS", fronters }); 66 + }); 67 + postTabObserver.observe(postTabElement, { attributes: true }); 68 + } else if (postTabObserver) { 69 + postTabObserver.disconnect(); 70 + postTabObserver = null; 71 + } 72 + // check for tab fronter for the current "post" 38 73 checkFronter(event.newUrl.toString()); 39 74 }); 40 75 41 76 // setup response "channel" 42 - console.log("sending setup message for response channel", respEventName); 43 77 document.dispatchEvent( 44 78 new CustomEvent("at-fronter-channel-setup", { 45 79 detail: respEventName, 46 80 }), 47 81 ); 48 82 49 - checkFronter(document.URL); 83 + // checkFronter(document.URL); 50 84 }, 51 85 });
+16 -3
src/entrypoints/popup/App.svelte
··· 2 2 import { expect } from "@/lib/result"; 3 3 import { getFronter } from "@/lib/utils"; 4 4 import { isResourceUri } from "@atcute/lexicons"; 5 - import type { ResourceUri } from "@atcute/lexicons/syntax"; 5 + import type { 6 + AtprotoDid, 7 + Handle, 8 + ResourceUri, 9 + } from "@atcute/lexicons/syntax"; 6 10 7 11 let recordAtUri = $state(""); 8 12 let queryResult = $state(""); 9 13 let isQuerying = $state(false); 10 14 let fronterName = $state(""); 15 + 16 + const makeOutput = (fronterName: string, handle: Handle | null) => { 17 + return `HANDLE: ${handle ?? "handle.invalid"}\nFRONTER: ${fronterName}`; 18 + }; 11 19 12 20 const queryRecord = async (recordUri: ResourceUri) => { 13 21 if (!recordAtUri.trim()) return; ··· 18 26 try { 19 27 if (!isResourceUri(recordUri)) throw "INVALID_RESOURCE_URI"; 20 28 const result = expect(await getFronter(recordUri)); 21 - queryResult = `FRONTER: ${result.name}` || "NO_FRONTER_FOUND"; 29 + queryResult = 30 + makeOutput(result.fronterName, result.handle) || 31 + "NO_FRONTER_FOUND"; 22 32 } catch (error) { 23 33 queryResult = `ERROR: ${error}`; 24 34 } finally { ··· 47 57 if (fronter) { 48 58 fronterName = fronter; 49 59 } 60 + 50 61 const tabs = await browser.tabs.query({ 51 62 active: true, 52 63 currentWindow: true, ··· 54 65 const tabFronter = await storage.getItem<{ 55 66 fronterName: string; 56 67 recordUri: ResourceUri; 68 + handle: Handle | null; 69 + did: AtprotoDid; 57 70 }>(`local:tab-${tabs[0].id!}-fronter`); 58 71 if (tabFronter) { 59 - queryResult = `FRONTER: ${tabFronter.fronterName}`; 72 + queryResult = makeOutput(tabFronter.fronterName, tabFronter.handle); 60 73 recordAtUri = tabFronter.recordUri; 61 74 } 62 75 });
+51 -20
src/lib/utils.ts
··· 5 5 } from "@atcute/lexicons"; 6 6 import { Client as AtpClient, simpleFetchHandler } from "@atcute/client"; 7 7 import { 8 + ActorIdentifier, 8 9 Did, 10 + Handle, 9 11 isHandle, 12 + RecordKey, 10 13 type AtprotoDid, 11 14 type ResourceUri, 12 15 } from "@atcute/lexicons/syntax"; ··· 20 23 WebDidDocumentResolver, 21 24 WellKnownHandleResolver, 22 25 } from "@atcute/identity-resolver"; 23 - import { DidDocument, getPdsEndpoint } from "@atcute/identity"; 26 + import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity"; 27 + 28 + export type Fronter = { 29 + fronterName: string; 30 + handle: Handle | null; 31 + did: AtprotoDid; 32 + }; 33 + 34 + export const fronterGetSocialAppHref = ( 35 + fronter: Fronter, 36 + rkey: RecordKey, 37 + depth?: number, 38 + ) => { 39 + return depth === 0 40 + ? `/profile/${fronter.handle ?? fronter.did}` 41 + : `/profile/${fronter.handle ?? fronter.did}/post/${rkey}`; 42 + }; 24 43 25 44 const fronterSchema = v.record( 26 45 v.string(), ··· 46 65 }, 47 66 }); 48 67 49 - // TODO: cache 68 + const resolveRepo = async (repo: ActorIdentifier) => { 69 + let handle: Handle | null; 70 + let did = repo; 71 + if (isHandle(repo)) { 72 + handle = repo; 73 + did = await handleResolver.resolve(repo); 74 + } else { 75 + const didDoc = await docResolver.resolve(repo as AtprotoDid); 76 + handle = getAtprotoHandle(didDoc) ?? null; 77 + } 78 + return { did: did as AtprotoDid, handle }; 79 + }; 80 + 50 81 const getAtpClient = async (repo: AtprotoDid) => { 51 82 const didDoc = await docResolver.resolve(repo); 52 83 const pdsUrl = getPdsEndpoint(didDoc); ··· 57 88 58 89 export const getFronter = async <Uri extends ResourceUri>( 59 90 recordUri: Uri, 60 - ): Promise<Result<InferOutput<typeof fronterSchema>, string>> => { 91 + ): Promise<Result<Fronter, string>> => { 61 92 const parsedRecordUri = parseResourceUri(recordUri); 62 93 if (!parsedRecordUri.ok) return err(parsedRecordUri.error); 63 94 64 95 // resolve repo 65 - let repo = parsedRecordUri.value.repo; 66 - if (isHandle(repo)) { 67 - // TODO: cache 68 - repo = await handleResolver.resolve(repo); 69 - } 96 + const { did, handle } = await resolveRepo(parsedRecordUri.value.repo); 70 97 71 98 // make client 72 - const atpClient = await getAtpClient(repo as AtprotoDid); 99 + const atpClient = await getAtpClient(did); 73 100 74 101 // fetch 75 102 let maybeRecord = await atpClient.get("com.atproto.repo.getRecord", { 76 103 params: { 77 - repo, 104 + repo: did, 78 105 collection: fronterSchema.object.shape.$type.expected, 79 106 rkey: `${parsedRecordUri.value.collection}_${parsedRecordUri.value.rkey}`, 80 107 }, ··· 86 113 const maybeTyped = safeParse(fronterSchema, maybeRecord.data.value); 87 114 if (!maybeTyped.ok) return err(maybeTyped.message); 88 115 89 - return maybeTyped; 116 + return ok({ 117 + fronterName: maybeTyped.value.name, 118 + handle, 119 + did, 120 + }); 90 121 }; 91 122 92 123 export const putFronter = async <Uri extends ResourceUri>( 93 124 recordUri: Uri, 94 125 name: string, 95 126 authToken: string, 96 - ): Promise<Result<void, string>> => { 127 + ): Promise<Result<Fronter, string>> => { 97 128 const parsedRecordUri = parseResourceUri(recordUri); 98 129 if (!parsedRecordUri.ok) return err(parsedRecordUri.error); 99 130 100 131 // resolve repo 101 - let repo = parsedRecordUri.value.repo; 102 - if (isHandle(repo)) { 103 - // TODO: cache 104 - repo = await handleResolver.resolve(repo); 105 - } 132 + const { did, handle } = await resolveRepo(parsedRecordUri.value.repo); 106 133 107 134 // make client 108 - const atpClient = await getAtpClient(repo as AtprotoDid); 135 + const atpClient = await getAtpClient(did); 109 136 110 137 // put 111 138 let maybeRecord = await atpClient.post("com.atproto.repo.putRecord", { 112 139 input: { 113 - repo, 140 + repo: did, 114 141 collection: fronterSchema.object.shape.$type.expected, 115 142 rkey: `${parsedRecordUri.value.collection}_${parsedRecordUri.value.rkey}`, 116 143 record: { name }, ··· 121 148 if (!maybeRecord.ok) 122 149 return err(maybeRecord.data.message ?? maybeRecord.data.error); 123 150 124 - return ok(undefined); 151 + return ok({ 152 + did, 153 + handle, 154 + fronterName: name, 155 + }); 125 156 };