Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/desktop/extensions/ai-sidebar/background-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() } } },
});
Expand Down
24 changes: 21 additions & 3 deletions apps/desktop/extensions/ai-sidebar/background.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
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();

// 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 })
Expand Down
53 changes: 44 additions & 9 deletions apps/desktop/extensions/ai-sidebar/w3bs.js
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -9,21 +12,53 @@
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://<name>, or the hop to
// w3bs.org/browse it redirects to, goes straight to https://<name>/. 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'],
},
},
];
47 changes: 46 additions & 1 deletion apps/desktop/extensions/ai-sidebar/w3bs.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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://<name> 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();
});
});
Loading