···77const action =
88 typeof browser !== "undefined" ? browser.browserAction : chrome.action
991010-// On extension installation, check if privacy consent was already accepted and show it if not
1111-runtime.onInstalled.addListener(() => {
1212- storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
1313- if (
1414- typeof privacyConsentAccepted === "undefined" ||
1515- !privacyConsentAccepted
1616- ) {
1717- tabs.create({ url: "privacy_consent.html" })
1818- }
1919- })
2020-})
1010+// The rest of the original background.js code...
1111+1212+// Content.js code migrated to background.js
1313+// Make sure that we don't DoS the regex if someone supplies too large of a DID
1414+const MAX_DID_LENGTH = 255
1515+1616+// Regular expression to validate the DID format
1717+// https://w3c.github.io/did-core/#did-syntax
1818+const didRegex =
1919+ /^did:plc:([a-zA-Z0-9._-]+(:[a-zA-Z0-9._-]+)*|((%[0-9A-Fa-f]{2})|[a-zA-Z0-9._-])+(:((%[0-9A-Fa-f]{2})|[a-zA-Z0-9._-])+)*$)/
2020+2121+// Function to validate the DID string
2222+function isValidDID(didString) {
2323+ return didString.length <= MAX_DID_LENGTH && didRegex.test(didString)
2424+}
2525+2626+// Function to get the domain name from the current hostname
2727+function getDomainName(url) {
2828+ const hostname = new URL(url).hostname
2929+ return hostname.replace(/^www\./, "")
3030+}
21312222-// If the message 'SHOW_CONSENT' is received, open the privacy consent tab
2323-runtime.onMessage.addListener((message) => {
2424- if (message.type === "SHOW_CONSENT") {
2525- tabs.create({ url: "privacy_consent.html" })
3232+// Function to validate the domain name
3333+function isValidDomain(domain) {
3434+ const MAX_DOMAIN_LENGTH = 255
3535+3636+ if (domain.length > MAX_DOMAIN_LENGTH) {
3737+ return false
2638 }
2727-})
3939+4040+ try {
4141+ // Use the build in URL constructor to validate the URL, if doesn't throw an error, the domain is valid
4242+ // This is a better choice than a regex since it should properly support punycode/international domains
4343+ new URL(`https://${domain}`)
4444+ return true
4545+ } catch (error) {
4646+ // The URL constructor threw an error, so the domain is not valid
4747+ return false
4848+ }
4949+}
5050+5151+// Function to check for a DID in the domain's TXT records
5252+async function checkForDIDDNS(domain) {
5353+ try {
5454+ const response = await fetch(
5555+ `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
5656+ )
5757+ const data = await response.json()
5858+5959+ // We use the TXT record type to avoid CORS issues
6060+ const records = data?.Answer?.filter((record) => record.type === 16) || []
6161+6262+ // We filter out all records that are not TXT records
6363+ const didRecord = records.find((record) =>
6464+ record.data.includes("did=did:plc:")
6565+ )
6666+6767+ // We return the DID if we found one and it's valid
6868+ return didRecord && isValidDID(didRecord.data.replace("did=", ""))
6969+ ? didRecord.data.replace("did=", "")
7070+ : null
7171+ } catch (error) {
7272+ return null
7373+ }
7474+}
7575+7676+// Function to check for a DID in the well-known (not .well-known) location
7777+async function checkForDIDHTTPS(domain) {
7878+ try {
7979+ const response = await fetch(
8080+ `https://${domain}/xrpc/com.atproto.identity.resolveHandle`
8181+ )
8282+8383+ if (!response.headers.get("Content-Type")?.includes("application/json")) {
8484+ throw new Error("Invalid Content-Type")
8585+ }
8686+ const data = await response.json()
8787+ return data.did && isValidDID(data.did) ? data.did : null
8888+ } catch (error) {
8989+ return null
9090+ }
9191+}
28922993// Map to store tabs with DIDs
3094const tabsWithDID = new Map()
31953296// URL of the Bluesky Web Applications
3333-const bskyAppUrl = "https://staging.bsky.app"
9797+const bskyAppUrl = "https://bsky.app"
34983599// Function to set the extension icon
36100function setIcon(tabId, iconName) {
37101 action.setIcon({ path: iconName, tabId })
38102}
39103104104+// Main function to perform actions, but only if the privacy consent has been accepted
105105+function performAction(tab) {
106106+ storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
107107+ // If the user has accepted the privacy consent
108108+ if (privacyConsentAccepted) {
109109+ const domain = getDomainName(tab.url)
110110+ if (isValidDomain(domain)) {
111111+ checkForDIDDNS(domain).then((domainDID) => {
112112+ if (domainDID) {
113113+ setIcon(tab.id, "logo48.png")
114114+ tabsWithDID.set(tab.id, domainDID)
115115+ } else {
116116+ checkForDIDHTTPS(domain).then((httpsDID) => {
117117+ if (httpsDID) {
118118+ setIcon(tab.id, "logo48.png")
119119+ tabsWithDID.set(tab.id, httpsDID)
120120+ } else {
121121+ setIcon(tab.id, "logo48_gray.png")
122122+ tabsWithDID.delete(tab.id)
123123+ }
124124+ })
125125+ }
126126+ })
127127+ }
128128+ }
129129+ })
130130+}
131131+132132+// Execute performAction when a tab is updated
133133+tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
134134+ if (changeInfo.status === "complete" && tab.active) {
135135+ performAction(tab)
136136+ }
137137+})
138138+139139+// On extension installation, check if privacy consent was already accepted and show it if not
140140+runtime.onInstalled.addListener(() => {
141141+ storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
142142+ if (
143143+ typeof privacyConsentAccepted === "undefined" ||
144144+ !privacyConsentAccepted
145145+ ) {
146146+ tabs.create({ url: "privacy_consent.html" })
147147+ }
148148+ })
149149+})
150150+40151// On extension installation, set the icon to gray for all tabs
41152runtime.onInstalled.addListener(() => {
42153 tabs.query({}, (tabs) => {
43154 tabs.forEach((tab) => setIcon(tab.id, "logo48_gray.png"))
44155 })
4545-})
4646-4747-// When a message is received from the DNS check, set the icon color to blue.
4848-runtime.onMessage.addListener((message, sender) => {
4949- if (message.type === "DID_FOUND") {
5050- setIcon(sender.tab.id, "logo48.png")
5151- tabsWithDID.set(sender.tab.id, message.did)
5252- } else {
5353- setIcon(sender.tab.id, "logo48_gray.png")
5454- tabsWithDID.delete(sender.tab.id)
5555- }
56156})
5715758158// Open the consent page if it hasn't been accepted and the user clicks on the extension icon
+2-3
manifest.json
···99 },
1010 "icons": { "48": "logo48.png", "128": "logo128.png" },
1111 "description": "Detects Decentralized Identifiers (DIDs) in a domain's TXT records and links to the associated Bluesky profile.",
1212- "permissions": ["activeTab", "storage"],
1313- "background": { "service_worker": "background.js" },
1414- "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.js"] }]
1212+ "permissions": ["tabs", "storage"],
1313+ "background": { "service_worker": "background.js" }
1514}