├── .gitignore ├── icons ├── icon_16px.png ├── icon_32px.png ├── icon_48px.png └── icon_180px.png ├── popup-animation.gif ├── docs ├── site-search.png ├── allow-incognito.png ├── search-engines.png ├── show-extensions.png ├── edit-search-engine.png ├── inactive-shortcuts.png └── unsupported-platform.png ├── LICENSE ├── popup.html ├── manifest.json ├── README.md └── service_worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | kagi_chrome_*.zip 2 | unpacked 3 | -------------------------------------------------------------------------------- /icons/icon_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/icons/icon_16px.png -------------------------------------------------------------------------------- /icons/icon_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/icons/icon_32px.png -------------------------------------------------------------------------------- /icons/icon_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/icons/icon_48px.png -------------------------------------------------------------------------------- /popup-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/popup-animation.gif -------------------------------------------------------------------------------- /docs/site-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/site-search.png -------------------------------------------------------------------------------- /icons/icon_180px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/icons/icon_180px.png -------------------------------------------------------------------------------- /docs/allow-incognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/allow-incognito.png -------------------------------------------------------------------------------- /docs/search-engines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/search-engines.png -------------------------------------------------------------------------------- /docs/show-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/show-extensions.png -------------------------------------------------------------------------------- /docs/edit-search-engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/edit-search-engine.png -------------------------------------------------------------------------------- /docs/inactive-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/inactive-shortcuts.png -------------------------------------------------------------------------------- /docs/unsupported-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/chrome_extension_basic/HEAD/docs/unsupported-platform.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 Kagi Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 | 8 | Remember to enable 'Allow in Incognito' in the browser extensions page! 9 | Here's a guide 14 |
15 |Learn more about this extension:
16 | 17 |Check out our other extensions for more features!
41 | 42 | 43 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Kagi Search", 4 | "version": "1.2.2.5", 5 | "description": "Set Kagi as your default search engine while also preserving your session in private browsing. Remember to enable 'Allow Incognito'.", 6 | "background": { 7 | "service_worker": "service_worker.js", 8 | "type": "module" 9 | }, 10 | "icons": { 11 | "16": "icons/icon_16px.png", 12 | "32": "icons/icon_32px.png", 13 | "48": "icons/icon_48px.png", 14 | "180": "icons/icon_180px.png" 15 | }, 16 | "action": { 17 | "default_icon": "icons/icon_32px.png", 18 | "default_title": "Kagi Search", 19 | "default_popup": "popup.html" 20 | }, 21 | "permissions": [ 22 | "contextMenus", 23 | "cookies", 24 | "declarativeNetRequestWithHostAccess", 25 | "webRequest", 26 | "storage" 27 | ], 28 | "host_permissions": ["https://*.kagi.com/*"], 29 | "chrome_settings_overrides": { 30 | "search_provider": { 31 | "name": "Kagi", 32 | "search_url": "https://kagi.com/search?q={searchTerms}", 33 | "favicon_url": "https://assets.kagi.com/v2/favicon-32x32.png", 34 | "keyword": "@kagi", 35 | "is_default": true, 36 | "suggest_url": "https://kagisuggest.com/api/autosuggest?q={searchTerms}", 37 | "encoding": "UTF-8" 38 | } 39 | }, 40 | "incognito": "spanning" 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kagi Chrome Extension 2 | 3 | This enables basic Kagi features for your Chromium browser (Chrome, Brave, Edge, Opera, [etc.](https://en.wikipedia.org/wiki/Chromium_(web_browser)#Browsers_based_on_Chromium)). 4 | 5 | Install from the [Chrome Web Store](https://chrome.google.com/webstore/detail/kagi-search-for-chrome/cdglnehniifkbagbbombnjghhcihifij) 6 | 7 | ## Features 8 | - Sets Kagi as your default search engine 9 | - Preserves your login across private browsing 10 | - Search-by-image by right-clicking on an image 11 | 12 | ## Additional Features 13 | Kagi has many more features to offer, unfortunately due to [Google Web Store Policies](https://developer.chrome.com/docs/webstore/troubleshooting/#single-use) these must be provided by a separate extension. This extension is currently under development. 14 | 15 | ## Setup 16 | 1. Navigate to the extension's settings page `chrome://extensions` 17 | 1. Click `Details` on the Kagi Search Extension  18 | 2. Scroll down and enable `Allow in Incognito`  19 | 20 | ## Platform Limitations 21 | As per [Google's Docs](https://developer.chrome.com/docs/extensions/reference/manifest/chrome-settings-override) the API to change the default search engine is only available on Windows and Mac. 22 | 23 | Accordingly, if you are developing the extension on Linux you may see the below error, which is safe to ignore. 24 | 25 |  26 | 27 | 28 | ## Setting Default Search on Linux 29 | Note that unfortunately, this does not provide autocompleting search features. 30 | 31 | 1. Navigate to [kagi.com](https://kagi.com) and if necessary, sign in. 32 | 2. Navigate to `chrome://settings/searchEngines`. 33 | 3. In the section `Inactive shortcuts` click `Activate` on the Kagi entry.  34 | 4. Now that Kagi has moved to the `Site Search` section, open the side menu and click `Make default`.  35 | 5. Kagi will now be in the `Search Engines` section.  36 | 6. For added privacy, click the 'pencil' icon to edit the Kagi entry. Replace the bottom field with `https://kagi.com/search?q=%s`. This step is not necessary but is recommended.  37 | -------------------------------------------------------------------------------- /service_worker.js: -------------------------------------------------------------------------------- 1 | const kagiBaseUrl = "https://kagi.com/"; 2 | let extensionToken = undefined; // use process memory to hold the token 3 | 4 | chrome.runtime.onInstalled.addListener((details) => { 5 | if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { 6 | chrome.tabs.create({ url: kagiBaseUrl }); 7 | chrome.contextMenus.create({ 8 | id: "kagi-image-search", 9 | title: "Kagi Image Search", 10 | contexts: ["image"], 11 | }); 12 | } 13 | }); 14 | 15 | async function loadTokenFromCookies() { 16 | const cookie = await chrome.cookies.get({ 17 | url: kagiBaseUrl, 18 | name: "kagi_session", 19 | }); 20 | 21 | if (!cookie || !cookie.value || cookie.value.trim().length === 0) { 22 | return; 23 | } 24 | return cookie.value; 25 | } 26 | 27 | async function applyHeader() { 28 | // check if PP mode is enabled, if so remove X-Kagi-Authorization header 29 | await requestPPMode(); 30 | const pp_mode_enabled = await isPPModeEnabled(); 31 | if (pp_mode_enabled) { 32 | await removeRules(); 33 | return; 34 | } 35 | 36 | // PP mode is not enabled, check if Token in Cookies changed 37 | const tokenFromCookies = await loadTokenFromCookies(); 38 | if (tokenFromCookies && tokenFromCookies !== extensionToken) { 39 | extensionToken = tokenFromCookies; 40 | } 41 | 42 | // finally apply X-Kagi-Authorization header with up-to-date Token value 43 | if (extensionToken) await updateRules(); 44 | } 45 | 46 | chrome.webRequest.onCompleted.addListener(applyHeader, { 47 | urls: ["https://*.kagi.com/*"], 48 | }); 49 | 50 | async function updateRules() { 51 | // https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest#dynamic-and-session-rules 52 | // dynamic needed as the token can't be known ahead of time 53 | await chrome.declarativeNetRequest.updateDynamicRules({ 54 | addRules: [ 55 | { 56 | id: 1, 57 | priority: 1, 58 | action: { 59 | type: "modifyHeaders", 60 | requestHeaders: [ 61 | { 62 | header: "X-Kagi-Authorization", 63 | value: extensionToken, 64 | operation: "set", 65 | }, 66 | ], 67 | }, 68 | condition: { 69 | urlFilter: `||kagi.com/`, 70 | resourceTypes: ["main_frame", "xmlhttprequest"], 71 | }, 72 | }, 73 | ], 74 | removeRuleIds: [1], 75 | }); 76 | } 77 | 78 | async function removeRules() { 79 | await chrome.declarativeNetRequest.updateDynamicRules({ 80 | addRules: [], 81 | removeRuleIds: [1], 82 | }); 83 | } 84 | 85 | // Image Search 86 | function kagiImageSearch(info) { 87 | const imageUrl = encodeURIComponent(info.srcUrl); 88 | chrome.tabs.create({ 89 | url: `${kagiBaseUrl}images?q=${imageUrl}&reverse=reference`, 90 | }); 91 | } 92 | 93 | 94 | chrome.contextMenus.onClicked.addListener((info, _) => { 95 | if (info.menuItemId === "kagi-image-search") { 96 | kagiImageSearch(info); 97 | } 98 | }); 99 | 100 | 101 | // Communication with Kagi Privacy Pass extension 102 | 103 | /* 104 | This extension makes the browser send a custom X-Kagi-Authorization header 105 | to kagi.com, to authenticate users even when using incognito mode. 106 | This can enter a "race condition" with the Kagi Privacy Pass extension, 107 | which strips all de-anonymising information sent to kagi.com, such as X-Kagi-Authorization, 108 | whenever "Privacy Pass mode" is in use. 109 | 110 | To avoid this race, we let the two extensions communicate, so that this extenesion removes 111 | (respectively, adds) the header when "Privacy Pass mode" is active (respectively, "PP mode" 112 | is inactive or the other extension is not installed/enabled). 113 | 114 | We achieve this syncronization with a simple messaging protocol outlined below: 115 | 116 | The Privacy Pass extension will send this extension single messages: 117 | - When being enabled (installed, activated) reports whether "PP mode" is enabled 118 | - When activating/deactivating "PP mode" 119 | Due to Chromium extension limitations, it cannot send a message when uninstalled/deactivated. 120 | 121 | The main extension (this one) keeps track of whether the "PP mode" is acrive or not by keeping state. 122 | This state is updated by the following actions: 123 | - When this extension is being enabled (installed, activated), it asks the PP extension for the "PP mode". 124 | - When it receives a status report from the PP extension, updates its state. 125 | 126 | Having both extensions send / request the "PP mode" status allows for the following: 127 | - When both are installed and active, whenever "PP mode" is toggled, this extension is informed and adjusts 128 | - Whenever one extension is installed, it attempts to sync with the other on whether "PP mode" is active 129 | 130 | There is one limitation, due to the PP extension being unable to signal to this one that it was uninstalled. 131 | This means that in theory, one could have a scenario where first PP mode is enabled, this extension removes 132 | X-Kagi-Authorization, and then the PP extension is uninstalled. In Incognito mode, where the kagi_session 133 | cookie is not sent by the browser, this would cause failed authentication with Kagi. 134 | 135 | Possible solutions: 136 | 1. have PP extension open a URL on uninstall, that signals this extension to update the header. This is possible 137 | but it means adding an extra new tab on uninstall. 138 | 2. Have this extension periodically poll whether the other one was uninstalled. This adds needless communication. 139 | Polling only when applying the header is not sufficient (as the PP extension could be uninstalled without 140 | webRequest.onComplete being triggered). 141 | 142 | In practice neither of these solutions seems necessary. Instead, we have this extension poll the PP extension every 143 | time it checks whether to apply the header. This means that even in the case where the PP extension is uninstalled while 144 | PP mode was set on, at most one query to kagi.com will fail to authenticate. Such query will then trigger webRequest.onComplete, 145 | which will then find out the PP extension was uninstalled, and hence reinstate X-Kagi-Authorize. 146 | */ 147 | 148 | const CHROME_KAGI_PRIVACY_PASS_EXTENSION = "mendokngpagmkejfpmeellpppjgbpdaj"; 149 | 150 | async function requestPPMode() { 151 | let pp_mode_enabled = false; 152 | try { 153 | pp_mode_enabled = await chrome.runtime.sendMessage(CHROME_KAGI_PRIVACY_PASS_EXTENSION, "status_report"); 154 | } catch (ex) { 155 | // other end does not exist, likely Privacy Pass extension disabled/not installed 156 | pp_mode_enabled = false; // PP mode not enabled 157 | } 158 | await chrome.storage.local.set({ "pp_mode_enabled": pp_mode_enabled }) 159 | } 160 | 161 | async function isPPModeEnabled() { 162 | const { pp_mode_enabled } = await chrome.storage.local.get({ "pp_mode_enabled": false }); 163 | return pp_mode_enabled; 164 | } 165 | 166 | // PP extension sent an unsolicited status report 167 | // We update our internal assumption, and update header application 168 | chrome.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { 169 | if (sender.id !== CHROME_KAGI_PRIVACY_PASS_EXTENSION) { 170 | // ignore messages from extensions other than the PP one 171 | return; 172 | } 173 | // check the message is about the PP mode 174 | if ('enabled' in request) { 175 | // update X-Kagi-Authorization header application 176 | await applyHeader(); 177 | } 178 | }); 179 | 180 | // when extension is started, ask for status report, and apply header accordingly 181 | chrome.runtime.onStartup.addListener(applyHeader) --------------------------------------------------------------------------------