@@ -9,18 +9,62 @@ function json(data: unknown, status = 200): Response {
99 } ) ;
1010}
1111
12- // Build recipient manually — in non-strict mode Mastodon's WebFinger requires
13- // HTTPS but our harness only has HTTP, so we use http:// for the inbox URL.
14- // In strict mode, Caddy terminates TLS, so we use https:// everywhere.
15- function parseRecipient (
12+ // Resolve a handle (user@domain) to the correct actor URI and inbox URL
13+ // via WebFinger + actor document fetch. Falls back to the Mastodon URL
14+ // convention (/users/{username}) when WebFinger is unavailable.
15+ const recipientCache = new Map < string , { inboxId : URL ; actorId : URL } > ( ) ;
16+
17+ async function parseRecipient (
1618 handle : string ,
17- ) : { inboxId : URL ; actorId : URL } {
19+ ) : Promise < { inboxId : URL ; actorId : URL } > {
20+ const cached = recipientCache . get ( handle ) ;
21+ if ( cached ) return cached ;
22+
1823 const [ user , domain ] = handle . split ( "@" ) ;
1924 const scheme = Deno . env . get ( "STRICT_MODE" ) ? "https" : "http" ;
25+
26+ // Try WebFinger resolution first — this discovers the correct actor URI
27+ // regardless of server software (Mastodon, Sharkey, etc.)
28+ try {
29+ const wfUrl = `${ scheme } ://${ domain } /.well-known/webfinger?resource=${
30+ encodeURIComponent ( `acct:${ handle } ` )
31+ } `;
32+ const wfRes = await fetch ( wfUrl , {
33+ headers : { Accept : "application/jrd+json" } ,
34+ } ) ;
35+ if ( wfRes . ok ) {
36+ const wf = await wfRes . json ( ) as {
37+ links ?: { rel : string ; type ?: string ; href ?: string } [ ] ;
38+ } ;
39+ const self = wf . links ?. find (
40+ ( l ) => l . rel === "self" && l . type === "application/activity+json" ,
41+ ) ;
42+ if ( self ?. href ) {
43+ const actorId = new URL ( self . href ) ;
44+ // Fetch the actor document to discover the inbox URL
45+ const actorRes = await fetch ( self . href , {
46+ headers : { Accept : "application/activity+json" } ,
47+ } ) ;
48+ if ( actorRes . ok ) {
49+ const actor = await actorRes . json ( ) as { inbox ?: string } ;
50+ if ( actor . inbox ) {
51+ const result = { inboxId : new URL ( actor . inbox ) , actorId } ;
52+ recipientCache . set ( handle , result ) ;
53+ return result ;
54+ }
55+ }
56+ }
57+ }
58+ } catch {
59+ // WebFinger failed; fall back to Mastodon convention
60+ }
61+
62+ // Fallback: construct URLs using Mastodon convention
2063 const inboxId = new URL ( `${ scheme } ://${ domain } /users/${ user } /inbox` ) ;
21- // Mastodon generates https:// actor URIs; use that as the canonical id
2264 const actorId = new URL ( `https://${ domain } /users/${ user } ` ) ;
23- return { inboxId, actorId } ;
65+ const result = { inboxId, actorId } ;
66+ recipientCache . set ( handle , result ) ;
67+ return result ;
2468}
2569
2670export async function handleBackdoor (
@@ -35,6 +79,7 @@ export async function handleBackdoor(
3579
3680 if ( url . pathname === "/_test/reset" && request . method === "POST" ) {
3781 store . clear ( ) ;
82+ recipientCache . clear ( ) ;
3883 return json ( { ok : true } ) ;
3984 }
4085
@@ -57,7 +102,7 @@ export async function handleBackdoor(
57102 undefined as void ,
58103 ) ;
59104
60- const { actorId, inboxId } = parseRecipient ( to ) ;
105+ const { actorId, inboxId } = await parseRecipient ( to ) ;
61106 const recipient = { id : actorId , inboxId } ;
62107
63108 const noteId = crypto . randomUUID ( ) ;
@@ -100,7 +145,7 @@ export async function handleBackdoor(
100145 undefined as void ,
101146 ) ;
102147
103- const { actorId, inboxId } = parseRecipient ( target ) ;
148+ const { actorId, inboxId } = await parseRecipient ( target ) ;
104149 const recipient = { id : actorId , inboxId } ;
105150
106151 const follow = new Follow ( {
@@ -134,7 +179,7 @@ export async function handleBackdoor(
134179 undefined as void ,
135180 ) ;
136181
137- const { actorId, inboxId } = parseRecipient ( target ) ;
182+ const { actorId, inboxId } = await parseRecipient ( target ) ;
138183 const recipient = { id : actorId , inboxId } ;
139184
140185 const undo = new Undo ( {
0 commit comments