this repo has no description
0
fork

Configure Feed

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

Background handle resolve.

alice 9a2cf990 7afc35fb

+559 -72
+6 -1
manifest.json
··· 17 17 "permissions": [ 18 18 "activeTab", 19 19 "contextMenus", 20 - "tabs" 20 + "tabs", 21 + "storage" 21 22 ], 22 23 "host_permissions": [ 23 24 "<all_urls>" 24 25 ], 26 + 27 + "background": { 28 + "service_worker": "service-worker.js" 29 + }, 25 30 26 31 "action": { 27 32 "default_popup": "popup.html",
+4
popup.html
··· 14 14 <body> 15 15 <p>Wormhole!</p> 16 16 <ul id="dest"></ul> 17 + <hr style="margin-top: 10px; margin-bottom: 10px;"/> 18 + <div style="text-align: center;"> 19 + <button id="emptyCacheBtn" style="padding: 4px 8px; font-size: 12px;">Empty Cache</button> 20 + </div> 17 21 <script src="transform.js"></script> 18 22 <script src="popup.js"></script> 19 23 </body>
+162 -17
popup.js
··· 1 1 (async function () { 2 + console.log("Popup script started"); // Log: Script start 2 3 const params = new URLSearchParams(window.location.search); 3 4 let raw = params.get("payload"); 5 + console.log("Initial raw payload:", raw); // Log: Raw payload 4 6 if (!raw) { 5 7 const [tab] = await chrome.tabs.query({ 6 8 active: true, 7 9 currentWindow: true, 8 10 }); 9 11 raw = tab.url; 12 + console.log("Raw payload from active tab:", raw); // Log: Tab URL payload 10 13 } 14 + 15 + const list = document.getElementById("dest"); 16 + list.innerHTML = "<li>Processing...</li>"; 17 + 18 + // Ensure WormholeTransform and its methods are available 19 + if ( 20 + !window.WormholeTransform || 21 + typeof window.WormholeTransform.parseInput !== "function" 22 + ) { 23 + list.innerHTML = "<li>Error: Transform script not loaded correctly.</li>"; 24 + console.error("Popup: WormholeTransform.parseInput is not available."); 25 + return; 26 + } 27 + 11 28 const info = await window.WormholeTransform.parseInput(raw); 12 - const list = document.getElementById("dest"); 13 - if (!info?.atUri) { 14 - list.innerHTML = "<li>No at:// reference found.</li>"; 29 + console.log("Parsed info:", JSON.stringify(info, null, 2)); // Log: Parsed info object 30 + 31 + if (!info || (!info.did && !info.atUri)) { 32 + list.innerHTML = "<li>No DID or at:// reference found in the input.</li>"; 33 + console.log("No DID or atUri found in info, exiting."); // Log: Exit condition 15 34 return; 16 35 } 17 - const dests = window.WormholeTransform.buildDestinations(info); 18 - list.innerHTML = dests 19 - .map( 20 - (d) => 21 - `<li> 22 - <a href="${d.url}" 23 - target="_blank" 24 - rel="noopener noreferrer" 25 - style="display:block; padding:6px 8px; border:1px solid #ccc; border-radius:6px; background:#fafafa; text-decoration:none; color:inherit; font-size:14px;"> 26 - ${d.label} 27 - </a> 28 - </li>` 29 - ) 30 - .join(""); 36 + 37 + const DID_HANDLE_CACHE_KEY = "didHandleCache"; // Caching re-enabled 38 + 39 + const renderDestinations = (destinations) => { 40 + console.log( 41 + "Rendering destinations:", 42 + JSON.stringify(destinations, null, 2) 43 + ); // Log: Destinations to render 44 + if (destinations && destinations.length > 0) { 45 + list.innerHTML = destinations 46 + .map( 47 + (d) => 48 + `<li> 49 + <a href="${d.url}" 50 + target="_blank" 51 + rel="noopener noreferrer" 52 + style="display:block; padding:6px 8px; border:1px solid #ccc; border-radius:6px; background:#fafafa; text-decoration:none; color:inherit; font-size:14px;"> 53 + ${d.label} 54 + </a> 55 + </li>` 56 + ) 57 + .join(""); 58 + } else { 59 + list.innerHTML = "<li>No actions available at this time.</li>"; 60 + console.log("No destinations to render."); // Log: No destinations 61 + } 62 + }; 63 + 64 + // Initial render based on whatever info.handle might exist (e.g. from direct handle input) 65 + let currentDests = window.WormholeTransform.buildDestinations(info); 66 + renderDestinations(currentDests); 67 + 68 + // If a DID is present but the handle is not, try to resolve or get from cache. 69 + if (info.did && !info.handle) { 70 + console.log( 71 + `Popup: DID ${info.did} present, handle missing. Checking cache...` 72 + ); 73 + 74 + const cacheData = await chrome.storage.local.get(DID_HANDLE_CACHE_KEY); 75 + const cachedHandles = cacheData[DID_HANDLE_CACHE_KEY] || {}; 76 + 77 + if (cachedHandles[info.did]) { 78 + console.log( 79 + `Popup: Found handle '${cachedHandles[info.did]}' for DID ${ 80 + info.did 81 + } in cache.` 82 + ); 83 + info.handle = cachedHandles[info.did]; 84 + currentDests = window.WormholeTransform.buildDestinations(info); 85 + renderDestinations(currentDests); 86 + } else { 87 + console.log( 88 + `Popup: Handle for DID ${info.did} not in cache. Attempting to resolve...` 89 + ); 90 + if (!currentDests || currentDests.length === 0) { 91 + list.innerHTML = 92 + "<li>Resolving identifier to check for more actions...</li>"; 93 + } 94 + try { 95 + if (typeof window.WormholeTransform.resolveDidToHandle !== "function") { 96 + list.innerHTML = "<li>Error: Resolve function not loaded.</li>"; 97 + console.error( 98 + "Popup: WormholeTransform.resolveDidToHandle is not available." 99 + ); 100 + return; 101 + } 102 + const resolvedHandle = 103 + await window.WormholeTransform.resolveDidToHandle(info.did); 104 + console.log( 105 + `Popup: Resolved handle: ${resolvedHandle} for DID: ${info.did}` 106 + ); 107 + if (resolvedHandle) { 108 + info.handle = resolvedHandle; 109 + currentDests = window.WormholeTransform.buildDestinations(info); 110 + console.log("Popup: Re-rendering destinations with resolved handle."); 111 + renderDestinations(currentDests); 112 + 113 + // Update cache with the newly resolved handle 114 + const updatedCachedHandles = { 115 + ...cachedHandles, 116 + [info.did]: resolvedHandle, 117 + }; 118 + await chrome.storage.local.set({ 119 + [DID_HANDLE_CACHE_KEY]: updatedCachedHandles, 120 + }); 121 + console.log( 122 + `Popup: Saved resolved handle ${resolvedHandle} for DID ${info.did} to cache.` 123 + ); 124 + } else { 125 + if (!currentDests || currentDests.length === 0) { 126 + list.innerHTML = 127 + "<li>Could not resolve identifier. No actions available.</li>"; 128 + } 129 + console.log( 130 + `Popup: Handle resolution returned null/false for DID: ${info.did}` 131 + ); 132 + } 133 + } catch (error) { 134 + console.error("Popup: Error resolving handle:", error); 135 + if (!currentDests || currentDests.length === 0) { 136 + list.innerHTML = 137 + "<li>Error resolving identifier. No actions available.</li>"; 138 + } 139 + } 140 + } 141 + } else if (info.did && info.handle) { 142 + console.log( 143 + "Popup: DID and handle already present in info, no resolution or cache check needed.", 144 + JSON.stringify(info, null, 2) 145 + ); 146 + } else { 147 + console.log( 148 + "Popup: Condition for handle resolution/cache check not met (no DID or handle already present)." 149 + ); 150 + } 151 + 152 + // Add event listener for the Empty Cache button 153 + const emptyCacheButton = document.getElementById("emptyCacheBtn"); 154 + if (emptyCacheButton) { 155 + emptyCacheButton.addEventListener("click", async () => { 156 + try { 157 + await chrome.storage.local.remove(DID_HANDLE_CACHE_KEY); 158 + console.log("Popup: DID Handle Cache Cleared!"); 159 + // Optionally, provide user feedback in the popup itself, e.g., change button text 160 + emptyCacheButton.textContent = "Cache Cleared!"; 161 + setTimeout(() => { 162 + emptyCacheButton.textContent = "Empty Cache"; 163 + }, 1500); 164 + // You might want to re-render or force a re-check if the current view depends on cached data 165 + // For now, it just clears it. Next time popup opens with a relevant DID, it will miss cache. 166 + } catch (error) { 167 + console.error("Popup: Error clearing cache:", error); 168 + emptyCacheButton.textContent = "Error Clearing!"; 169 + } 170 + }); 171 + } else { 172 + console.warn("Popup: Empty Cache button not found."); 173 + } 174 + 175 + console.log("Popup script finished (caching re-enabled)."); // Log: Script end 31 176 })();
+56
service-worker.js
··· 1 + // service-worker.js 2 + 3 + // Try to import transform.js. 4 + // In Manifest V3, for service workers, direct global function exposure from importScripts is typical. 5 + // If transform.js is structured to put its functions on a global (e.g., self.WormholeTransform), 6 + // we'd use that. Assuming it makes functions globally available for now. 7 + try { 8 + importScripts('transform.js'); 9 + } catch (e) { 10 + console.error("Failed to import transform.js in service-worker:", e); 11 + } 12 + 13 + const DID_HANDLE_CACHE_KEY = 'didHandleCache'; // Caching re-enabled 14 + 15 + chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 16 + // Ensure functions are available (they should be if importScripts worked) 17 + if (typeof parseInput !== 'function' || typeof resolveDidToHandle !== 'function') { 18 + console.error('Service Worker: parseInput or resolveDidToHandle not defined. Check transform.js import.'); 19 + return; 20 + } 21 + 22 + if (changeInfo.status === 'complete' && tab.url) { 23 + console.log(`Service Worker: Tab ${tabId} updated to complete, URL: ${tab.url}`); 24 + try { 25 + const info = await parseInput(tab.url); 26 + if (info && info.did && !info.handle) { // We have a DID but no immediate handle 27 + console.log(`Service Worker: Found DID ${info.did} from ${tab.url}, attempting to resolve handle for pre-caching.`); 28 + 29 + // Check if we already have this DID in cache to avoid unnecessary re-fetching by the service worker itself 30 + // The popup will have its own logic to prefer cache then fetch. 31 + const cache = await chrome.storage.local.get(DID_HANDLE_CACHE_KEY); 32 + const currentCache = cache[DID_HANDLE_CACHE_KEY] || {}; 33 + if (currentCache[info.did]) { 34 + console.log(`Service Worker: Handle for DID ${info.did} already in cache, no pre-fetch needed by worker.`); 35 + return; 36 + } 37 + 38 + const resolvedHandle = await resolveDidToHandle(info.did); 39 + 40 + if (resolvedHandle) { 41 + console.log(`Service Worker: Successfully resolved DID ${info.did} to handle ${resolvedHandle}. Caching.`); 42 + const newCacheEntry = { [info.did]: resolvedHandle }; 43 + const updatedCache = { ...currentCache, ...newCacheEntry }; 44 + await chrome.storage.local.set({ [DID_HANDLE_CACHE_KEY]: updatedCache }); 45 + console.log(`Service Worker: DID ${info.did} -> Handle ${resolvedHandle} saved to cache.`); 46 + } else { 47 + console.log(`Service Worker: Could not resolve handle for DID ${info.did} from ${tab.url}. Not caching.`); 48 + } 49 + } 50 + } catch (error) { 51 + console.error(`Service Worker: Error processing tab update for ${tab.url}:`, error); 52 + } 53 + } 54 + }); 55 + 56 + console.log("Service Worker started and listeners added (caching re-enabled).");
+55 -31
test.js
··· 3 3 const assert = require("assert").strict; 4 4 5 5 function mockFetchResponse(data = {}, isOk = true, httpStatus = 200) { 6 - return () => Promise.resolve({ 7 - ok: isOk, 8 - status: httpStatus, 9 - json: () => Promise.resolve(data), 10 - }); 6 + return () => 7 + Promise.resolve({ 8 + ok: isOk, 9 + status: httpStatus, 10 + json: () => Promise.resolve(data), 11 + }); 11 12 } 12 13 13 14 const fetchMockConfigs = [ ··· 25 26 "@context": [ 26 27 "https://www.w3.org/ns/did/v1", 27 28 "https://w3id.org/security/multikey/v1", 28 - "https://w3id.org/security/suites/secp256k1-2019/v1" 29 + "https://w3id.org/security/suites/secp256k1-2019/v1", 29 30 ], 30 - "id": "did:web:didweb.watch", 31 - "alsoKnownAs": [ 32 - "at://didweb.watch" 33 - ], 34 - "verificationMethod": [ 31 + id: "did:web:didweb.watch", 32 + alsoKnownAs: ["at://didweb.watch"], 33 + verificationMethod: [ 35 34 { 36 - "id": "did:web:didweb.watch#atproto", 37 - "type": "Multikey", 38 - "controller": "did:web:didweb.watch", 39 - "publicKeyMultibase": "zQ3shPLyZu2EbgJ75P61bMZP4yvBwmtd22ph5sEnY6oLz4YLo" 40 - } 35 + id: "did:web:didweb.watch#atproto", 36 + type: "Multikey", 37 + controller: "did:web:didweb.watch", 38 + publicKeyMultibase: 39 + "zQ3shPLyZu2EbgJ75P61bMZP4yvBwmtd22ph5sEnY6oLz4YLo", 40 + }, 41 41 ], 42 - "service": [ 42 + service: [ 43 43 { 44 - "id": "#atproto_pds", 45 - "type": "AtprotoPersonalDataServer", 46 - "serviceEndpoint": "https://zio.blue" 47 - } 48 - ] 44 + id: "#atproto_pds", 45 + type: "AtprotoPersonalDataServer", 46 + serviceEndpoint: "https://zio.blue", 47 + }, 48 + ], 49 49 }), 50 50 }, 51 51 { ··· 59 59 ]; 60 60 61 61 global.fetch = (url) => { 62 - const mock = fetchMockConfigs.find(m => m.condition(url)); 62 + const mock = fetchMockConfigs.find((m) => m.condition(url)); 63 63 if (mock) { 64 64 return mock.response(); 65 65 } 66 66 console.warn(`Unhandled fetch mock for URL: ${url}`); 67 - return Promise.resolve({ ok: false, status: 500, json: () => Promise.resolve({}) }); 67 + return Promise.resolve({ 68 + ok: false, 69 + status: 500, 70 + json: () => Promise.resolve({}), 71 + }); 68 72 }; 69 73 70 74 async function run() { ··· 89 93 name: "parseInput/feed/cozy", 90 94 input: "https://deer.social/profile/why.bsky.team/feed/cozy", 91 95 expected: { 92 - atUri: "at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/cozy", 96 + atUri: 97 + "at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/cozy", 93 98 did: "did:plc:vpkhqolt662uhesyj6nxm7ys", 94 99 handle: "why.bsky.team", 95 100 rkey: "cozy", ··· 99 104 }, 100 105 { 101 106 name: "parseInput/feed.post", 102 - input: "https://deer.social/profile/did:plc:kkkcb7sys7623hcf7oefcffg/post/3lpe6ek6xhs2n", 107 + input: 108 + "https://deer.social/profile/did:plc:kkkcb7sys7623hcf7oefcffg/post/3lpe6ek6xhs2n", 103 109 expected: { 104 - atUri: "at://did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lpe6ek6xhs2n", 110 + atUri: 111 + "at://did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lpe6ek6xhs2n", 105 112 did: "did:plc:kkkcb7sys7623hcf7oefcffg", 106 113 handle: null, 107 114 rkey: "3lpe6ek6xhs2n", 108 115 nsid: "app.bsky.feed.post", 109 - bskyAppPath: "/profile/did:plc:kkkcb7sys7623hcf7oefcffg/post/3lpe6ek6xhs2n", 116 + bskyAppPath: 117 + "/profile/did:plc:kkkcb7sys7623hcf7oefcffg/post/3lpe6ek6xhs2n", 110 118 }, 111 119 }, 112 120 { 113 121 name: "parseInput/lists", 114 - input: "https://deer.social/profile/alice.mosphere.at/lists/3l7vfhhfqcz2u", 122 + input: 123 + "https://deer.social/profile/alice.mosphere.at/lists/3l7vfhhfqcz2u", 115 124 expected: { 116 - atUri: "at://did:plc:by3jhwdqgbtrcc7q4tkkv3cf/app.bsky.graph.list/3l7vfhhfqcz2u", 125 + atUri: 126 + "at://did:plc:by3jhwdqgbtrcc7q4tkkv3cf/app.bsky.graph.list/3l7vfhhfqcz2u", 117 127 did: "did:plc:by3jhwdqgbtrcc7q4tkkv3cf", 118 128 handle: "alice.mosphere.at", 119 129 rkey: "3l7vfhhfqcz2u", ··· 135 145 }, 136 146 { 137 147 name: "parseInput/did:web/post", 138 - input: "https://deer.social/profile/did:web:didweb.watch/post/3lpaioe62qk2j", 148 + input: 149 + "https://deer.social/profile/did:web:didweb.watch/post/3lpaioe62qk2j", 139 150 expected: { 140 151 atUri: "at://did:web:didweb.watch/app.bsky.feed.post/3lpaioe62qk2j", 141 152 did: "did:web:didweb.watch", ··· 143 154 rkey: "3lpaioe62qk2j", 144 155 nsid: "app.bsky.feed.post", 145 156 bskyAppPath: "/profile/did:web:didweb.watch/post/3lpaioe62qk2j", 157 + }, 158 + }, 159 + { 160 + name: "parseInput/queryParam/did", 161 + input: 162 + "https://boat.kelinci.net/plc-oplogs?q=did:plc:5sk4eqsu7byvwokfcnfgywxg", 163 + expected: { 164 + atUri: "at://did:plc:5sk4eqsu7byvwokfcnfgywxg", 165 + did: "did:plc:5sk4eqsu7byvwokfcnfgywxg", 166 + handle: null, 167 + nsid: undefined, 168 + rkey: undefined, 169 + bskyAppPath: "/profile/did:plc:5sk4eqsu7byvwokfcnfgywxg", 146 170 }, 147 171 }, 148 172 ];
+276 -23
transform.js
··· 11 11 if (!str.startsWith("http")) { 12 12 return await canonicalize(str); 13 13 } 14 - // Try to pull out at://... from inside web URL, else treat full URL as possible at link 15 - const m = str.match(/at:\/\/[\w:.\-\/]+/); 16 - if (m) { 17 - return await canonicalize(m[0]); 14 + 15 + // Try to pull out at://... from inside web URL 16 + const atMatch = str.match(/at:\/\/[\w:.\-\/]+/); 17 + if (atMatch) { 18 + return await canonicalize(atMatch[0]); 18 19 } 19 - const parts = str.split(/[/?#]/); 20 - for (let i = 0; i < parts.length; i++) { 21 - const p = parts[i]; 22 - if ( 23 - p.startsWith("did:") || 24 - (p.includes(".") && parts[i - 1] === "profile") 25 - ) { 26 - // Re-attach everything that comes *after* the id so we keep /post/<rkey> 27 - const rest = parts.slice(i + 1).join("/"); 28 - return await canonicalize(p + (rest ? "/" + rest : "")); 20 + 21 + try { 22 + const url = new URL(str); 23 + // Check for 'q' query parameter containing a DID 24 + const qParam = url.searchParams.get("q"); 25 + if (qParam && qParam.startsWith("did:")) { 26 + return await canonicalize(qParam); 27 + } 28 + 29 + // Existing logic to find DID/handle in path parts 30 + const parts = str.split(/[/?#]/); 31 + for (let i = 0; i < parts.length; i++) { 32 + const p = parts[i]; 33 + if ( 34 + p.startsWith("did:") || 35 + (p.includes(".") && parts[i - 1] === "profile") 36 + ) { 37 + const rest = parts.slice(i + 1).join("/"); 38 + return await canonicalize(p + (rest ? "/" + rest : "")); 39 + } 29 40 } 41 + } catch (e) { 42 + // If URL parsing fails or other error, log it and fall through or return null 43 + console.error("Error parsing URL or its components:", e); 44 + // Depending on desired behavior, you might want to return null here 45 + // or let it fall through if there's a chance non-URL at:// strings are passed with http prefix 30 46 } 47 + 31 48 return null; 32 49 } 33 50 ··· 41 58 let did = idPart.startsWith("did:") ? idPart : null; 42 59 const handle = did ? null : idPart; 43 60 if (!did && handle) { 44 - did = await resolveHandle(handle); 61 + // Use the renamed function here 62 + did = await resolveHandleToDid(handle); 45 63 } 46 64 47 65 if (restParts.length) { ··· 59 77 if (acct) { 60 78 bskyAppPath = `/profile/${acct}`; 61 79 if (nsid && rkey) { 62 - if (nsid === "app.bsky.feed.generator") { 63 - bskyAppPath += `/feed/${rkey}`; 64 - } else if (nsid === "app.bsky.feed.post") { 65 - bskyAppPath += `/post/${rkey}`; 66 - } else if (nsid === "app.bsky.graph.list") { 67 - bskyAppPath += `/lists/${rkey}`; 80 + // Find the shortcut key (e.g., "feed", "post") that matches the current full nsid 81 + const shortcutKey = Object.keys(NSID_SHORTCUTS).find( 82 + (key) => NSID_SHORTCUTS[key] === nsid 83 + ); 84 + if (shortcutKey) { 85 + bskyAppPath += `/${shortcutKey}/${rkey}`; 68 86 } 87 + // If nsid is not in NSID_SHORTCUTS (e.g. a custom nsid), 88 + // bskyAppPath remains /profile/acct, which is the current behavior. 69 89 } 70 90 } 71 91 ··· 82 102 }; 83 103 } 84 104 85 - async function resolveHandle(handle) { 105 + // Renamed function: Takes a handle, returns a DID 106 + async function resolveHandleToDid(handle) { 86 107 // If handle is a did:web:... and looks like did:web:domain 87 108 if (typeof handle === "string" && handle.startsWith("did:web:")) { 88 109 const parts = handle.split(":"); ··· 119 140 } 120 141 } 121 142 143 + // Helper function to extract handle from alsoKnownAs array 144 + function _extractHandleFromAlsoKnownAs(alsoKnownAs) { 145 + if (Array.isArray(alsoKnownAs)) { 146 + for (const aka of alsoKnownAs) { 147 + if (typeof aka === "string" && aka.startsWith("at://")) { 148 + const handle = aka.substring("at://".length); 149 + if (handle) { 150 + return handle; 151 + } 152 + } 153 + } 154 + } 155 + return null; 156 + } 157 + 158 + // Helper function to construct the .well-known/did.json URL for did:web 159 + function _getDidWebWellKnownUrl(did) { 160 + // Decode percent-encoded characters in the method-specific identifier part 161 + const methodSpecificId = decodeURIComponent( 162 + did.substring("did:web:".length).split("#")[0] 163 + ); 164 + const parts = methodSpecificId.split(":"); 165 + const hostAndPort = parts[0]; // e.g., "example.com" or "localhost:3000" 166 + let path = ""; 167 + if (parts.length > 1) { 168 + // Path segments are joined by slashes 169 + path = "/" + parts.slice(1).join("/"); 170 + } 171 + // Ensure path doesn't end with a slash if it's not empty before appending /.well-known 172 + if (path && path.endsWith("/")) { 173 + path = path.slice(0, -1); 174 + } 175 + return `https://${hostAndPort}${path}/.well-known/did.json`; 176 + } 177 + 178 + // New function: Takes a DID, returns a handle 179 + async function resolveDidToHandle(did) { 180 + if (!did || typeof did !== "string") { 181 + console.warn( 182 + "resolveDidToHandle: Invalid DID provided (null or not a string)", 183 + did 184 + ); 185 + return null; 186 + } 187 + 188 + // --- DID:PLC --- 189 + if (did.startsWith("did:plc:")) { 190 + console.log(`resolveDidToHandle: Processing did:plc: ${did}`); 191 + // Primary: plc.directory 192 + try { 193 + const plcUrl = `https://plc.directory/${encodeURIComponent(did)}`; 194 + console.log("resolveDidToHandle: Fetching from PLC directory:", plcUrl); 195 + const plcResp = await fetch(plcUrl); 196 + if (plcResp.ok) { 197 + const plcData = await plcResp.json(); 198 + console.log( 199 + "resolveDidToHandle: PLC directory success for", 200 + did, 201 + "; Data snippet:", 202 + JSON.stringify(plcData).substring(0, 200) + "..." 203 + ); 204 + if (plcData.alsoKnownAs) { 205 + const handleFromPlc = _extractHandleFromAlsoKnownAs( 206 + plcData.alsoKnownAs 207 + ); 208 + if (handleFromPlc) { 209 + console.log( 210 + `resolveDidToHandle: Found handle '${handleFromPlc}' from plc.directory alsoKnownAs for ${did}` 211 + ); 212 + return handleFromPlc; 213 + } 214 + } 215 + } else { 216 + const errorText = await plcResp 217 + .text() 218 + .catch(() => "Failed to get error text from plcResp"); 219 + console.warn( 220 + `resolveDidToHandle: plc.directory fetch failed for ${did} with status ${plcResp.status}. Error: ${errorText}` 221 + ); 222 + } 223 + } catch (error) { 224 + console.error( 225 + `resolveDidToHandle: Error fetching/parsing PLC directory for ${did}:`, 226 + error 227 + ); 228 + } 229 + 230 + // Fallback to describeRepo for did:plc: 231 + console.log( 232 + `resolveDidToHandle: Falling back to describeRepo for did:plc: ${did}` 233 + ); 234 + try { 235 + const drUrl = `https://public.api.bsky.app/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent( 236 + did 237 + )}`; 238 + console.log( 239 + "resolveDidToHandle: Attempting describeRepo fetch (plc fallback):", 240 + drUrl 241 + ); 242 + const drResp = await fetch(drUrl); 243 + if (drResp.ok) { 244 + const drData = await drResp.json(); 245 + console.log( 246 + "resolveDidToHandle: describeRepo success for", 247 + did, 248 + "(plc fallback); Data:", 249 + JSON.stringify(drData) 250 + ); 251 + if (drData.handle) { 252 + console.log( 253 + `resolveDidToHandle: Found handle '${drData.handle}' from describeRepo (plc fallback) for ${did}` 254 + ); 255 + return drData.handle; 256 + } 257 + } 258 + } catch (error) { 259 + console.error( 260 + `resolveDidToHandle: Error in describeRepo (plc fallback) for ${did}:`, 261 + error 262 + ); 263 + } 264 + 265 + console.log( 266 + `resolveDidToHandle: All resolution methods failed for did:plc: ${did}` 267 + ); 268 + return null; 269 + } 270 + 271 + // --- DID:WEB --- 272 + else if (did.startsWith("did:web:")) { 273 + console.log(`resolveDidToHandle: Processing did:web: ${did}`); 274 + const methodSpecificId = decodeURIComponent( 275 + did.substring("did:web:".length).split("#")[0] 276 + ); 277 + 278 + // Primary: .well-known/did.json 279 + try { 280 + const wellKnownUrl = _getDidWebWellKnownUrl(did); 281 + console.log( 282 + "resolveDidToHandle: Fetching from .well-known/did.json:", 283 + wellKnownUrl 284 + ); 285 + const wkResp = await fetch(wellKnownUrl); 286 + if (wkResp.ok) { 287 + const wkData = await wkResp.json(); 288 + console.log( 289 + "resolveDidToHandle: .well-known success for", 290 + did, 291 + "; Data snippet:", 292 + JSON.stringify(wkData).substring(0, 200) + "..." 293 + ); 294 + if (wkData.alsoKnownAs) { 295 + const handleFromWk = _extractHandleFromAlsoKnownAs( 296 + wkData.alsoKnownAs 297 + ); 298 + if (handleFromWk) { 299 + console.log( 300 + `resolveDidToHandle: Found handle '${handleFromWk}' from .well-known alsoKnownAs for ${did}` 301 + ); 302 + return handleFromWk; 303 + } 304 + } 305 + } else { 306 + const errorText = await wkResp 307 + .text() 308 + .catch(() => "Failed to get error text from wkResp"); 309 + console.warn( 310 + `resolveDidToHandle: .well-known fetch failed for ${did} (url: ${wellKnownUrl}) status ${wkResp.status}. Error: ${errorText}` 311 + ); 312 + } 313 + } catch (error) { 314 + console.error( 315 + `resolveDidToHandle: Error fetching/parsing .well-known/did.json for ${did}:`, 316 + error 317 + ); 318 + } 319 + 320 + // Fallback 1: describeRepo 321 + console.log( 322 + `resolveDidToHandle: Falling back to describeRepo for did:web: ${did}` 323 + ); 324 + try { 325 + const drUrl = `https://public.api.bsky.app/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent( 326 + did 327 + )}`; 328 + console.log( 329 + "resolveDidToHandle: Attempting describeRepo fetch (web fallback 1):", 330 + drUrl 331 + ); 332 + const drResp = await fetch(drUrl); 333 + if (drResp.ok) { 334 + const drData = await drResp.json(); 335 + console.log( 336 + "resolveDidToHandle: describeRepo success for", 337 + did, 338 + "(web fallback 1); Data:", 339 + JSON.stringify(drData) 340 + ); 341 + if (drData.handle) { 342 + console.log( 343 + `resolveDidToHandle: Found handle '${drData.handle}' from describeRepo (web fallback 1) for ${did}` 344 + ); 345 + return drData.handle; 346 + } 347 + } 348 + } catch (error) { 349 + console.error( 350 + `resolveDidToHandle: Error in describeRepo (web fallback 1) for ${did}:`, 351 + error 352 + ); 353 + } 354 + 355 + // Fallback 2: Derive from did:web string (methodSpecificId) 356 + console.log( 357 + `resolveDidToHandle: All other methods failed for did:web: ${did}. Falling back to derived handle: ${methodSpecificId}` 358 + ); 359 + return methodSpecificId; 360 + } 361 + 362 + // --- UNSUPPORTED DID METHOD --- 363 + else { 364 + console.warn( 365 + `resolveDidToHandle: Unsupported DID method for ${did}. Only did:plc and did:web are supported.` 366 + ); 367 + return null; 368 + } 369 + } 370 + 122 371 function buildDestinations(info) { 123 372 // Use the new bskyAppPath for both deer.social and bsky.app 124 373 const { atUri, did, handle, rkey, bskyAppPath } = info; ··· 183 432 NSID_SHORTCUTS, 184 433 parseInput, 185 434 canonicalize, 186 - resolveHandle, 435 + resolveHandleToDid, // Export with new name 436 + resolveDidToHandle, // Export new function 187 437 buildDestinations, 188 438 }; 189 439 } else if (typeof window !== "undefined") { 190 440 window.WormholeTransform = { 191 441 parseInput, 442 + canonicalize, 443 + resolveHandleToDid, // Export with new name for window object 444 + resolveDidToHandle, // Export new function for window object 192 445 buildDestinations, 193 446 }; 194 447 }