···991010## [Unreleased]
11111212-- Input validation for domains and DIDs (security enhancement)
1212+- Migrate/refactor everything from `content.js` into a self-contained `background.js`
1313+- Swap `activeTab` permission for `tabs` so that we can drop the `<all_urls>` permission
1314- Remove permissions for "management" as it is not needed
1515+- Migrate Firefox to Manifest v3 (but this means the minimum FF version is now 109)
1616+- Input validation for domains and DIDs (security enhancement)
1717+- Replace `staging.bsky.app` with `bsky.app`
14181519## [1.3.0]
1620
+4-4
README.md
···15151616---
17171818-As of April 30, 2023, Bluesky is still invite-only and the web app is at https://staging.bsky.app.
1818+As of May 17, 2023, Bluesky is still invite-only and the web app is at https://bsky.app.
19192020-You can find me there at [@adhdjesse.com](https://staging.bsky.app/profile/adhdjesse.com)
2020+You can find me there at [@adhdjesse.com](https://bsky.app/profile/adhdjesse.com)
21212222**Contributors:**
23232424-- [@danielhuckmann.com](https://staging.bsky.app/profile/danielhuckmann.com) - Firefox Support & Privacy Consent
2525-- [@aliceisjustplaying](https://staging.bsky.app/profile/alice.bsky.sh) - HTTPS Method of DID Detection
2424+- [@danielhuckmann.com](https://bsky.app/profile/danielhuckmann.com) - Firefox Support, Privacy & Security Enhancements
2525+- [@aliceisjustplaying](https://bsky.app/profile/alice.bsky.sh) - HTTPS Method of DID Detection
+1-5
background.js
···44const tabs = typeof browser !== "undefined" ? browser.tabs : chrome.tabs
55const storage =
66 typeof browser !== "undefined" ? browser.storage.local : chrome.storage.local
77-const action =
88- typeof browser !== "undefined" ? browser.browserAction : chrome.action
99-1010-// The rest of the original background.js code...
77+const action = typeof browser !== "undefined" ? browser.action : chrome.action
1181212-// Content.js code migrated to background.js
139// Make sure that we don't DoS the regex if someone supplies too large of a DID
1410const MAX_DID_LENGTH = 255
1511
-130
content.js
···11-// Set up cross-browser compatibility
22-const runtime =
33- typeof browser !== "undefined" ? browser.runtime : chrome.runtime
44-const storage =
55- typeof browser !== "undefined" ? browser.storage.local : chrome.storage.local
66-77-// Make sure that we don't DoS the regex if someone supplies too large of a DID
88-const MAX_DID_LENGTH = 255
99-1010-// Regular expression to validate the DID format
1111-// https://w3c.github.io/did-core/#did-syntax
1212-const didRegex =
1313- /^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._-])+)*$)/
1414-1515-// Function to validate the DID string
1616-function isValidDID(didString) {
1717- return didString.length <= MAX_DID_LENGTH && didRegex.test(didString)
1818-}
1919-2020-// Function to get the domain name from the current hostname
2121-function getDomainName() {
2222- const hostname = window.location.hostname
2323- return hostname.replace(/^www\./, "")
2424-}
2525-2626-// Function to validate the domain name
2727-function isValidDomain(domain) {
2828- const MAX_DOMAIN_LENGTH = 255
2929-3030- if (domain.length > MAX_DOMAIN_LENGTH) {
3131- return false
3232- }
3333-3434- try {
3535- // Use the build in URL constructor to validate the URL, if doesn't throw an error, the domain is valid
3636- // This is a better choice than a regex since it should properly support punycode/international domains
3737- new URL(`https://${domain}`)
3838- return true
3939- } catch (error) {
4040- // The URL constructor threw an error, so the domain is not valid
4141- return false
4242- }
4343-}
4444-4545-// Function to check for a DID in the domain's TXT records
4646-async function checkForDIDDNS(domain) {
4747- try {
4848- const response = await fetch(
4949- `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
5050- )
5151- const data = await response.json()
5252-5353- // We use the TXT record type to avoid CORS issues
5454- const records = data?.Answer?.filter((record) => record.type === 16) || []
5555-5656- // We filter out all records that are not TXT records
5757- const didRecord = records.find((record) =>
5858- record.data.includes("did=did:plc:")
5959- )
6060-6161- // We return the DID if we found one and it's valid
6262- return didRecord && isValidDID(didRecord.data.replace("did=", ""))
6363- ? didRecord.data.replace("did=", "")
6464- : null
6565- } catch (error) {
6666- return null
6767- }
6868-}
6969-7070-// Function to check for a DID in the well-known (not .well-known) location
7171-async function checkForDIDHTTPS(domain) {
7272- try {
7373- const response = await fetch(
7474- `https://${domain}/xrpc/com.atproto.identity.resolveHandle`
7575- )
7676-7777- if (!response.headers.get("Content-Type")?.includes("application/json")) {
7878- throw new Error("Invalid Content-Type")
7979- }
8080-8181- const data = await response.json()
8282- return data.did && isValidDID(data.did) ? data.did : null
8383- } catch (error) {
8484- return null
8585- }
8686-}
8787-8888-// Main function to perform actions, but only if the privacy consent has been accepted
8989-function performAction(privacyConsentAccepted) {
9090- // If the user has accepted the privacy consent
9191- if (privacyConsentAccepted) {
9292- // We check for a DID on the current domain
9393- ;(async function () {
9494- const domain = getDomainName()
9595- if (isValidDomain(domain)) {
9696- const domainDID = await checkForDIDDNS(domain)
9797- const httpsDID = await checkForDIDHTTPS(domain)
9898-9999- if (domainDID) {
100100- runtime.sendMessage({ type: "DID_FOUND", did: domainDID })
101101- } else if (httpsDID) {
102102- runtime.sendMessage({ type: "DID_FOUND", did: httpsDID })
103103- } else {
104104- runtime.sendMessage({ type: "DID_NOT_FOUND" })
105105- }
106106- }
107107- })()
108108-109109- // We listen for messages from the background script
110110- runtime.onMessage.addListener((message, sender, sendResponse) => {
111111- if (message.type === "GET_DID") {
112112- const domain = getDomainName()
113113- if (isValidDomain(domain)) {
114114- checkForDIDDNS(domain)
115115- .then((did) => sendResponse({ did }))
116116- .catch(() => sendResponse({ did: null }))
117117- return true // Indicate that the response will be sent asynchronously.
118118- }
119119- }
120120- })
121121- } else {
122122- // Do nothing since the consent form has not been accepted.
123123- return
124124- }
125125-}
126126-127127-// Get the user's privacy consent from the storage and perform actions accordingly
128128-storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
129129- performAction(privacyConsentAccepted)
130130-})