···7788## [Unreleased]
991010+## [1.2.0] - 2023-05-03
1111+1212+- Adds support for Firefox
1313+- Adds privacy consent dialog for Google DNS (required by Mozilla)
1414+1015## [1.1.0] - 2023-05-03
11161217- use DID for profile url instead of domain name
+4
README.md
···1717As of April 30, 2023, Bluesky is still invite-only and the web app is at https://staging.bsky.app.
18181919You can find me there at [@adhdjesse.com](https://staging.bsky.app/profile/adhdjesse.com)
2020+2121+**Contributors:**
2222+2323+[@danielhuckmann.com](https://staging.bsky.app/profile/danielhuckmann.com) - Firefox Support & Privacy Consent
+47-19
background.js
···11-const tabsWithDID = new Map()
11+// Set up cross-browser compatibility
22+const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime;
33+const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs;
44+const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;
55+const browserAction = typeof browser !== 'undefined' ? browser.browserAction : chrome.browserAction;
66+77+// On extension installation, check if privacy consent was already accepted and show it if not
88+runtime.onInstalled.addListener(() => {
99+ storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
1010+ if (typeof privacyConsentAccepted === "undefined" || !privacyConsentAccepted) {
1111+ tabs.create({ url: "privacy_consent.html" });
1212+ }
1313+ });
1414+});
1515+1616+// If the message 'SHOW_CONSENT' is received, open the privacy consent tab
1717+runtime.onMessage.addListener((message, sender, sendResponse) => {
1818+ if (message.type === 'SHOW_CONSENT') {
1919+ tabs.create({ url: "privacy_consent.html" });
2020+ }
2121+});
22233-const bskyAppUrl = "https://staging.bsky.app"
2323+// Map to store tabs with DIDs
2424+const tabsWithDID = new Map();
4252626+// URL of the Bluesky Web Applications
2727+const bskyAppUrl = 'https://staging.bsky.app';
2828+2929+// Function to set the extension icon
530function setIcon(tabId, iconName) {
66- chrome.action.setIcon({ path: iconName, tabId })
3131+ browserAction.setIcon({ path: iconName, tabId });
732}
83399-chrome.runtime.onInstalled.addListener(() => {
1010- chrome.tabs.query({}, (tabs) => {
1111- tabs.forEach((tab) => setIcon(tab.id, "logo48_gray.png"))
1212- })
1313-})
3434+// On extension installation, set the icon to gray for all tabs
3535+runtime.onInstalled.addListener(() => {
3636+ tabs.query({}, (tabs) => {
3737+ tabs.forEach((tab) => setIcon(tab.id, 'logo48_gray.png'));
3838+ });
3939+});
14401515-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
4141+// When a message is received from the DNS check, set the icon color to blue.
4242+runtime.onMessage.addListener((message, sender, sendResponse) => {
1643 if (message.type === "DID_FOUND") {
1717- setIcon(sender.tab.id, "logo48.png")
1818- tabsWithDID.set(sender.tab.id, message.did)
4444+ setIcon(sender.tab.id, "logo48.png");
4545+ tabsWithDID.set(sender.tab.id, message.did);
1946 } else {
2020- setIcon(sender.tab.id, "logo48_gray.png")
2121- tabsWithDID.delete(sender.tab.id)
4747+ setIcon(sender.tab.id, "logo48_gray.png");
4848+ tabsWithDID.delete(sender.tab.id);
2249 }
2323-})
5050+});
24512525-chrome.action.onClicked.addListener((tab) => {
2626- const did = tabsWithDID.get(tab.id)
5252+// When the extension icon is clicked, open the profile page if there's a DID
5353+browserAction.onClicked.addListener((tab) => {
5454+ const did = tabsWithDID.get(tab.id);
2755 if (did) {
2828- const newUrl = `${bskyAppUrl}/profile/${did}`
2929- chrome.tabs.create({ url: newUrl })
5656+ const newUrl = `${bskyAppUrl}/profile/${did}`;
5757+ tabs.create({ url: newUrl });
3058 }
3131-})
5959+});
+50-36
content.js
···11-function getDomainName() {
22- const hostname = window.location.hostname
33- return hostname.replace(/^www\./, "")
44-}
11+// Set up cross-browser compatibility
22+const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime;
33+const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs;
44+const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;
5566-async function checkForDID(domain) {
77- // We use Google's DNS over HTTPS API to resolve the TXT record
88- const response = await fetch(
99- `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
1010- )
1111- const data = await response.json()
66+// Function to perform actions based on the user's privacy consent
77+function performAction(privacyConsentAccepted) {
88+ // If the user has accepted the privacy consent
99+ if (privacyConsentAccepted) {
1010+ // Function to get the domain name from the current hostname
1111+ function getDomainName() {
1212+ const hostname = window.location.hostname;
1313+ return hostname.replace(/^www\./, '');
1414+ }
12151313- // We use the TXT record type to avoid CORS issues
1414- const records = data?.Answer?.filter((record) => record.type === 16) || []
1616+ // Function to check for a DID in the domain's TXT records
1717+ async function checkForDID(domain) {
1818+ const response = await fetch(
1919+ `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
2020+ );
2121+ const data = await response.json();
15221616- // We filter out all records that are not TXT records
1717- const didRecord = records.find((record) =>
1818- record.data.includes("did=did:plc:")
1919- )
2323+ const records = data?.Answer?.filter((record) => record.type === 16) || [];
20242121- // We return the DID if we found one
2222- return didRecord ? didRecord.data.replace("did=", "") : null
2323-}
2525+ const didRecord = records.find((record) =>
2626+ record.data.includes("did=did:plc:")
2727+ );
24282525-// We check for a DID on the current domain
2626-;(async function () {
2727- const domain = getDomainName()
2828- const did = await checkForDID(domain)
2929+ return didRecord ? didRecord.data.replace("did=", "") : null;
3030+ }
3131+3232+ // Immediately invoked function to check for a DID and send a message based on the result
3333+ (async function () {
3434+ const domain = getDomainName();
3535+ const did = await checkForDID(domain);
29363030- if (did) {
3131- chrome.runtime.sendMessage({ type: "DID_FOUND", did })
3737+ if (did) {
3838+ runtime.sendMessage({ type: "DID_FOUND", did });
3939+ } else {
4040+ runtime.sendMessage({ type: "DID_NOT_FOUND" });
4141+ }
4242+ })();
4343+4444+ // Listener for the 'GET_DOMAIN' message and respond with the domain
4545+ runtime.onMessage.addListener((message, sender, sendResponse) => {
4646+ if (message.type === 'GET_DOMAIN') {
4747+ sendResponse({ domain: getDomainName() });
4848+ }
4949+ });
3250 } else {
3333- chrome.runtime.sendMessage({ type: "DID_NOT_FOUND" })
5151+ // If the user hasn't accepted the privacy consent, show the consent page again
5252+ tabs.create({ url: "privacy_consent.html" });
3453 }
3535-})()
5454+}
36553737-// We listen for messages from the background script
3838-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3939- if (message.type === "GET_DID") {
4040- checkForDID(getDomainName())
4141- .then((did) => sendResponse({ did }))
4242- .catch(() => sendResponse({ did: null }))
4343- return true // Indicate that the response will be sent asynchronously.
4444- }
4545-})
5656+// Get the user's privacy consent from the storage and perform actions accordingly
5757+storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
5858+ performAction(privacyConsentAccepted);
5959+});
+98
privacy_consent.html
···11+<!DOCTYPE html>
22+<html lang="en">
33+<head>
44+ <meta charset="UTF-8">
55+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
66+ <title>Skylink Privacy Consent</title>
77+ <style>
88+ html {
99+ line-height: 1.15;
1010+ -webkit-text-size-adjust: 100%;
1111+ }
1212+1313+ body {
1414+ margin: 0;
1515+ }
1616+1717+ h1 {
1818+ font-size: 2em;
1919+ margin: 0.67em 0;
2020+ }
2121+2222+ a {
2323+ background-color: transparent;
2424+ }
2525+2626+ p {
2727+ margin: 1em 0;
2828+ }
2929+3030+ ul {
3131+ margin: 1em 0;
3232+ padding: 0 0 0 40px;
3333+ }
3434+3535+ li {
3636+ list-style-type: disc;
3737+ margin: 0 0 0.25em 0;
3838+ }
3939+4040+ button {
4141+ font-family: inherit;
4242+ font-size: 100%;
4343+ line-height: 1.15;
4444+ margin: 0;
4545+ overflow: visible;
4646+ text-transform: none;
4747+ }
4848+4949+ button::-moz-focus-inner {
5050+ border-style: none;
5151+ padding: 0;
5252+ }
5353+5454+ button:-moz-focusring {
5555+ outline: 1px dotted ButtonText;
5656+ }
5757+ .centered-container {
5858+ position: absolute;
5959+ top: 50%;
6060+ left: 50%;
6161+ transform: translate(-50%, -50%);
6262+ }
6363+ </style>
6464+</head>
6565+<body>
6666+ <div class="centered-container">
6767+ <h1>Privacy Consent for SkyLink - Bluesky DID Detector</h1>
6868+ <p>While <b>no information is collected by the extension author</b>, this extension makes use of the <a href="https://developers.google.com/speed/public-dns/">Google DNS service</a> for required functionality.</p>
6969+ <p>The Google DNS service is used because browser extensions do not have native access to DNS, which is needed to lookup the AT PROTO TXT record.</p>
7070+ <p>A summary of what is collected by Google DNS:
7171+ <ul>
7272+ <li>the IP address of your device sending the DNS query</li>
7373+ <li>Requested domain name</li>
7474+ <li>Request type (TXT)</li>
7575+ <li>Request Size</li>
7676+ <li>Transport protocol (HTTPS)</li>
7777+ <li>Client's autonomous system number</li>
7878+ <li>User's geolocation: country, region, and city (no more specific than 1 km² and 1000 users)</li>
7979+ <li>DNS Response code</li>
8080+ <li>Google DNS server information</li>
8181+ <li>Timestamp</li>
8282+ <li>Processing time</li>
8383+ <li>Response DNS flags (including AD, CD, DO, RD, and TC)</li>
8484+ <li>Response size</li>
8585+ <li>EDNS version</li>
8686+ <li>EDNS option</li>
8787+ <li>EDNS Client Subnet (ECS) (IP protocol and prefix length -- excluding the client IP address)</li>
8888+ <li>Version string corresponding to HTTP path (/resolve)</li>
8989+ <li>Response HTTP encoding, such as application/dns-message or json</li>
9090+ </ul>
9191+ </p>
9292+ <p><a href="https://developers.google.com/speed/public-dns/privacy" target="_blank">Please Read and accept the full Google DNS privacy policy before using this addon.</a></p>
9393+ <button id="accept">Accept (Continue)</button>
9494+ <button id="decline">Decline (Remove Extension)</button>
9595+ <script src="privacy_consent.js"></script>
9696+ </div>
9797+</body>
9898+</html>