From 4844285c5db169e006db52917c145484278cbb3d Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 20 Oct 2026 01:54:05 +0000 Subject: [PATCH] w3bs://chovy.hacker opens chovy.hacker itself when Pit is on With Pit on, Moshpit names resolve in this browser, so there is no reason to land on the pit.world gateway. While pitEnabled, session declarativeNetRequest rules send a search for w3bs://, or the hop to w3bs.org/browse it redirects to, straight to https:///; the tab watcher does the same for prefetched searches. Pit off is unchanged. Chromium cannot keep w3bs:// in the address bar: that needs a scheme handler in the engine itself. Co-Authored-By: Claude Opus 5.5 --- .../ai-sidebar/background-helper.test.js | 3 +- .../extensions/ai-sidebar/background.js | 24 +++++++-- apps/desktop/extensions/ai-sidebar/w3bs.js | 53 +++++++++++++++---- .../extensions/ai-sidebar/w3bs.test.js | 47 +++++++++++++++- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/apps/desktop/extensions/ai-sidebar/background-helper.test.js b/apps/desktop/extensions/ai-sidebar/background-helper.test.js index 6364fb2..a1601c4 100644 --- a/apps/desktop/extensions/ai-sidebar/background-helper.test.js +++ b/apps/desktop/extensions/ai-sidebar/background-helper.test.js @@ -20,8 +20,9 @@ describe('extension network helper requests', () => { onMessage: { addListener: (listener) => listeners.push(listener) }, sendMessage: done(), }, - storage: { local: storage(), session: storage() }, + storage: { local: storage(), session: storage(), onChanged: { addListener: vi.fn() } }, tabs: { onUpdated: { addListener: vi.fn() }, update: done() }, + declarativeNetRequest: { updateSessionRules: done() }, proxy: { settings: { set: done(), clear: done() } }, privacy: { network: { webRTCIPHandlingPolicy: { set: done(), clear: done() } } }, }); diff --git a/apps/desktop/extensions/ai-sidebar/background.js b/apps/desktop/extensions/ai-sidebar/background.js index 75a8fac..e930a59 100644 --- a/apps/desktop/extensions/ai-sidebar/background.js +++ b/apps/desktop/extensions/ai-sidebar/background.js @@ -1,7 +1,7 @@ import { decideInstallTarget, lookupInstalled } from './install-state.js'; import { PIT_SOCKS_PORT, pitProxyConfig } from './pit-proxy.js'; import { installPush } from './push-client.js'; -import { w3bsFromSearch } from './w3bs.js'; +import { PIT_DIRECT_RULES, w3bsFromSearch } from './w3bs.js'; // Web Push through TronBrowser's configured push service (the engine has none). installPush(); @@ -9,11 +9,29 @@ installPush(); // A w3bs:// URI typed in the address bar becomes a web search. w3bs-rules.json // redirects that search before it loads, but not when the omnibox serves it // from its search prefetch, so catch whatever a tab lands on as well. -chrome.tabs.onUpdated.addListener((tabId, change) => { - const target = change.url && w3bsFromSearch(change.url); +chrome.tabs.onUpdated.addListener(async (tabId, change) => { + if (!change.url || !/w3bs/i.test(change.url)) return; + const { pitEnabled } = await chrome.storage.local.get('pitEnabled'); + const target = w3bsFromSearch(change.url, { pit: !!pitEnabled }); if (target) chrome.tabs.update(tabId, { url: target }).catch(() => {}); }); +// With Pit on, w3bs://chovy.hacker opens chovy.hacker itself instead of its +// pit.world gateway. Session rules, kept in step with the toggle. +async function syncW3bsPitRules() { + const { pitEnabled } = await chrome.storage.local.get('pitEnabled'); + await chrome.declarativeNetRequest.updateSessionRules({ + removeRuleIds: PIT_DIRECT_RULES.map((rule) => rule.id), + addRules: pitEnabled ? PIT_DIRECT_RULES : [], + }); +} +syncW3bsPitRules().catch((err) => console.warn('w3bs pit rules:', err)); +chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && 'pitEnabled' in changes) { + syncW3bsPitRules().catch((err) => console.warn('w3bs pit rules:', err)); + } +}); + // Open the AI side panel when the toolbar action is clicked. chrome.sidePanel .setPanelBehavior({ openPanelOnActionClick: true }) diff --git a/apps/desktop/extensions/ai-sidebar/w3bs.js b/apps/desktop/extensions/ai-sidebar/w3bs.js index 5d84ec7..f466d21 100644 --- a/apps/desktop/extensions/ai-sidebar/w3bs.js +++ b/apps/desktop/extensions/ai-sidebar/w3bs.js @@ -1,6 +1,9 @@ // W3BS URIs (w3bs://type/namespace/name@version) open through the w3bs.org // resolver page: Chromium has no handler for the scheme. The launcher and the -// w3bs-links.js content script use the same gateway. +// w3bs-links.js content script use the same gateway. w3bs://chovy.hacker names +// a site rather than a resource, and the resolver opens it at its pit.world +// gateway; with Pit on, Moshpit names resolve in this browser, so it opens as +// itself (PIT_DIRECT_RULES). export const W3BS_GATEWAY = 'https://w3bs.org/browse?q='; /** The resolver URL for a w3bs: URI, or null when `input` is not one. */ @@ -9,21 +12,53 @@ export function w3bsUrl(input) { return /^w3bs:/i.test(uri) ? W3BS_GATEWAY + encodeURIComponent(uri) : null; } +/** https://chovy.hacker/path for w3bs://chovy.hacker/path (a dotted authority is a site); else null. */ +export function w3bsSiteUrl(input) { + const m = /^w3bs:\/\/([a-z0-9-]+(?:\.[a-z0-9-]+)+)(\/\S*)?$/i.exec(String(input ?? '').trim()); + return m ? `https://${m[1].toLowerCase()}${m[2] || '/'}` : null; +} + +/** Where a w3bs: URI opens: the site itself when Pit is on and it names one, else the resolver. */ +export function w3bsTarget(input, { pit = false } = {}) { + return (pit && w3bsSiteUrl(input)) || w3bsUrl(input); +} + /** - * The resolver URL for a search results page whose query is a W3BS URI, or - * null. Chromium searches a typed w3bs://chovy.hacker instead of opening it, - * and when the omnibox serves that search from its prefetch the - * declarativeNetRequest rule in w3bs-rules.json never sees a request, so the - * background worker also checks every URL a tab lands on. + * Where to send a tab that landed on a search results page whose query is a + * W3BS URI (or, with Pit on, on the resolver page for a site), or null. + * Chromium searches a typed w3bs://chovy.hacker instead of opening it, and when + * the omnibox serves that search from its prefetch the declarativeNetRequest + * rules never see a request, so the background worker also checks every URL a + * tab lands on. */ -export function w3bsFromSearch(href) { +export function w3bsFromSearch(href, { pit = false } = {}) { let url; try { url = new URL(href); } catch { return null; } - if (url.protocol !== 'https:' || url.hostname === 'w3bs.org') return null; + if (url.protocol !== 'https:') return null; const q = url.searchParams.get('q'); - return q ? w3bsUrl(q) : null; + if (!q) return null; + if (url.hostname === 'w3bs.org') return pit && url.pathname === '/browse' ? w3bsSiteUrl(q) : null; + return w3bsTarget(q, { pit }); } + +// Session rules while Pit is on: a search for w3bs://, or the hop to +// w3bs.org/browse it redirects to, goes straight to https:///. Higher +// priority than w3bs-rules.json, which sends every w3bs:// search to w3bs.org. +// A path is left to the resolver: the rule cannot percent-decode it. +export const PIT_DIRECT_RULES = [ + { + id: 101, + priority: 2, + action: { type: 'redirect', redirect: { regexSubstitution: 'https://\\1/' } }, + condition: { + regexFilter: + '^https://[^/]+/[^?#]*\\?(?:[^#]*&)?q=w3bs(?:%3A|:)(?:%2F|/){2}([a-z0-9-]+(?:\\.[a-z0-9-]+)+)(?:%2F|/)?(?:[&#].*)?$', + isUrlFilterCaseSensitive: false, + resourceTypes: ['main_frame'], + }, + }, +]; diff --git a/apps/desktop/extensions/ai-sidebar/w3bs.test.js b/apps/desktop/extensions/ai-sidebar/w3bs.test.js index 301861c..ec8a272 100644 --- a/apps/desktop/extensions/ai-sidebar/w3bs.test.js +++ b/apps/desktop/extensions/ai-sidebar/w3bs.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { w3bsFromSearch, w3bsUrl } from './w3bs.js'; +import { PIT_DIRECT_RULES, w3bsFromSearch, w3bsSiteUrl, w3bsUrl } from './w3bs.js'; describe('w3bsUrl', () => { it('sends a W3BS URI to the w3bs.org resolver', () => { @@ -37,4 +37,49 @@ describe('w3bsFromSearch', () => { expect(w3bsFromSearch('chrome://newtab/')).toBeNull(); expect(w3bsFromSearch('not a url')).toBeNull(); }); + + it('with Pit on, opens a named site itself, from the search or the resolver hop', () => { + const pit = { pit: true }; + expect(w3bsFromSearch('https://pit.world/search?q=w3bs%3A%2F%2Fchovy.hacker', pit)).toBe('https://chovy.hacker/'); + expect(w3bsFromSearch('https://w3bs.org/browse?q=w3bs%3A%2F%2FChovy.Hacker%2Fblog', pit)).toBe( + 'https://chovy.hacker/blog', + ); + // Resources still go to the resolver; w3bs.org pages that are not a site stay put. + expect(w3bsFromSearch('https://pit.world/search?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch', pit)).toBe( + 'https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch', + ); + expect(w3bsFromSearch('https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch', pit)).toBeNull(); + }); +}); + +describe('w3bsSiteUrl', () => { + it('reads a dotted authority as a site, and nothing else', () => { + expect(w3bsSiteUrl('w3bs://chovy.hacker')).toBe('https://chovy.hacker/'); + expect(w3bsSiteUrl('w3bs://example.com/a?b=1')).toBe('https://example.com/a?b=1'); + expect(w3bsSiteUrl('w3bs://prompt/w3bs/research@1')).toBeNull(); + expect(w3bsSiteUrl('https://chovy.hacker')).toBeNull(); + }); +}); + +describe('PIT_DIRECT_RULES', () => { + // Chromium's regexFilter is RE2; this pattern is plain enough to read the same in JS. + const rule = PIT_DIRECT_RULES[0]; + const re = new RegExp(rule.condition.regexFilter, 'i'); + const apply = (href) => { + const m = re.exec(href); + return m ? rule.action.redirect.regexSubstitution.replace('\\1', m[1]) : null; + }; + + it('sends a w3bs:// search, or its resolver hop, to the name itself', () => { + expect(apply('https://pit.world/search?q=w3bs%3A%2F%2Fchovy.hacker')).toBe('https://chovy.hacker/'); + expect(apply('https://duckduckgo.com/?q=w3bs%3A%2F%2Fchovy.hacker&ia=web')).toBe('https://chovy.hacker/'); + expect(apply('https://w3bs.org/browse?q=w3bs%3A%2F%2Fchovy.hacker')).toBe('https://chovy.hacker/'); + expect(apply('https://w3bs.org/browse?q=w3bs://chovy.hacker')).toBe('https://chovy.hacker/'); + }); + + it('leaves resources, paths and other searches to the resolver', () => { + expect(apply('https://pit.world/search?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch')).toBeNull(); + expect(apply('https://pit.world/search?q=w3bs%3A%2F%2Fchovy.hacker%2Fblog')).toBeNull(); + expect(apply('https://pit.world/search?q=chovy.hacker')).toBeNull(); + }); });