experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(protocol): rename extensionPaths map to tile vocabulary

The map in protocol.ts is keyed off built-in feature IDs (= tiles in v2)
and dev-loaded tiles registered via --load-extension. Chrome browser
extensions go through Electron's `session.loadExtension()` API and
never touch this map (see `chrome-extensions.ts`). The "extension"
naming was an artifact of the v1 extension model where tiles were
called extensions.

Renames:
- `extensionPaths` (private Map) → `tilePaths`
- `registerExtensionPath()` → `registerTilePath()`
- `getExtensionPath()` → `getTilePath()`
- `getRegisteredExtensionIds()` → `getRegisteredTileIds()`

Adjacent comments updated: "extension folder" → "tile folder",
"extension not found" → "tile not found", "stale extension window"
→ "stale tile window", per-handler docstrings clarified that the
peek://ext/{id}/ URL prefix is a legacy URL shape resolving to the
tile registry (the resolver is one and the same).

Callers updated in main.ts, ipc.ts (drops the now-unused
`getExtensionPath` import), tile-compat.ts, tile-launcher.ts,
session.ts, and the index.ts re-export surface.

yarn build clean. yarn test:unit: 2277 pass / 0 fail.

+64 -64
+3 -3
backend/electron/index.ts
··· 74 74 APP_SCHEME, 75 75 APP_PROTOCOL, 76 76 registerScheme, 77 - registerExtensionPath, 78 - getExtensionPath, 79 - getRegisteredExtensionIds, 77 + registerTilePath, 78 + getTilePath, 79 + getRegisteredTileIds, 80 80 registerThemePath, 81 81 getThemePath, 82 82 getRegisteredThemeIds,
-1
backend/electron/ipc.ts
··· 37 37 38 38 39 39 import { 40 - getExtensionPath, 41 40 getRegisteredThemeIds, 42 41 getThemePath, 43 42 getActiveThemeId,
+10 -10
backend/electron/main.ts
··· 11 11 import { pathToFileURL } from 'node:url'; 12 12 13 13 import { initDatabase, closeDatabase, getDb, getContextEntry } from './datastore.js'; 14 - import { registerScheme, initProtocol, registerExtensionPath, getExtensionPath, getRegisteredExtensionIds, registerThemePath, getRegisteredThemeIds } from './protocol.js'; 14 + import { registerScheme, initProtocol, registerTilePath, getTilePath, getRegisteredTileIds, registerThemePath, getRegisteredThemeIds } from './protocol.js'; 15 15 import { initCore, getCoreBackgroundWindow } from './core-glue.js'; 16 16 import { initTestFixture } from './test-fixture-glue.js'; 17 17 import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled } from './extensions.js'; ··· 595 595 export function discoverBuiltinExtensions(extensionsDir: string): void { 596 596 const discovered = discoverExtensions(extensionsDir); 597 597 for (const ext of discovered) { 598 - registerExtensionPath(ext.id, ext.path); 598 + registerTilePath(ext.id, ext.path); 599 599 } 600 600 } 601 601 ··· 813 813 // Add core renderers (cmd, hud, page) — always running once app is up 814 814 for (const extId of CORE_RENDERER_IDS) { 815 815 if (!isBuiltinExtensionEnabled(extId)) continue; 816 - const extPath = getExtensionPath(extId); 816 + const extPath = getTilePath(extId); 817 817 const manifest = extPath ? loadExtensionManifest(extPath) : null; 818 818 running.push({ 819 819 id: extId, ··· 842 842 * Used by the Settings Features pane to show ALL extensions from the extensions/ directory. 843 843 */ 844 844 export function getAllRegisteredExtensions(): Array<{ id: string; manifest: unknown; status: string }> { 845 - const registeredIds = getRegisteredExtensionIds(); 845 + const registeredIds = getRegisteredTileIds(); 846 846 const result: Array<{ id: string; manifest: unknown; status: string }> = []; 847 847 848 - // Build a set of running extension IDs for status info. 848 + // Build a set of running tile IDs for status info. 849 849 // Sources: core renderers (always up) and loaded v2 tiles. 850 850 const runningSet = new Set<string>(CORE_RENDERER_IDS); 851 851 for (const tileId of getLoadedTileIds()) { ··· 853 853 } 854 854 855 855 for (const extId of registeredIds) { 856 - const extPath = getExtensionPath(extId); 856 + const extPath = getTilePath(extId); 857 857 const manifest = extPath ? loadExtensionManifest(extPath) : null; 858 858 result.push({ 859 859 id: extId, ··· 879 879 */ 880 880 export function registerDevExtension(extId: string, extPath: string): void { 881 881 devExtensions.set(extId, { path: extPath }); 882 - // Also register the path so protocol handler can find it 883 - registerExtensionPath(extId, extPath); 882 + // Also register the path so the protocol handler can resolve peek://{extId}/... 883 + registerTilePath(extId, extPath); 884 884 DEBUG && console.log(`[ext:dev] Registered dev extension: ${extId} from ${extPath}`); 885 885 } 886 886 ··· 1601 1601 handleLocalShortcut, 1602 1602 initTray, 1603 1603 getDb, 1604 - getExtensionPath, 1605 - getRegisteredExtensionIds, 1604 + getTilePath, 1605 + getRegisteredTileIds, 1606 1606 loadExtensionManifest, 1607 1607 };
+41 -40
backend/electron/protocol.ts
··· 3 3 * 4 4 * Handles: 5 5 * - peek://app/{path} - Core app files 6 - * - peek://ext/{ext-id}/{path} - Extension content 7 - * - peek://extensions/{path} - Shared extension infrastructure 6 + * - peek://ext/{tile-id}/{path} - Tile content (legacy URL prefix; tiles ARE the unit of a feature) 7 + * - peek://extensions/{path} - Shared tile infrastructure (resolves under features/) 8 8 * - peek://theme/{path} - Current theme files (vars.css, manifest.json) 9 9 * - peek://theme/{themeId}/{path} - Specific theme files 10 10 * - peek://thumbnail/{hash} - Page screenshot thumbnails (JPEG) ··· 27 27 28 28 // Lazy-load `protocol` and `net` via CommonJS require so this module can be 29 29 // imported under ELECTRON_RUN_AS_NODE=1 (where electron's named ESM exports 30 - // are empty). Unit tests that only consume pure helpers (e.g. extension path 31 - // resolution via getExtensionPath) never touch the lazy binding. 30 + // are empty). Unit tests that only consume pure helpers (e.g. tile path 31 + // resolution via getTilePath) never touch the lazy binding. 32 32 const requireElectron = createRequire(import.meta.url); 33 33 let _electron: typeof import('electron') | null = null; 34 34 function getElectron(): typeof import('electron') { ··· 45 45 { get: (_t, prop) => (getElectron().net as any)[prop] } 46 46 ); 47 47 48 - // Extension path cache: extensionId -> filesystem path 49 - const extensionPaths = new Map<string, string>(); 48 + // Tile path cache: tileId -> filesystem path. Populated when a v2 tile 49 + // is registered (tile-compat) or when a dev tile is loaded via 50 + // --load-extension. Used by the per-tile origin resolver below 51 + // (`peek://{tileId}/...`) and by anyone needing a tile's on-disk path. 52 + const tilePaths = new Map<string, string>(); 50 53 51 54 // Theme path cache: themeId -> filesystem path 52 55 const themePaths = new Map<string, string>(); ··· 175 178 } 176 179 177 180 /** 178 - * Register a built-in extension path 181 + * Register a built-in tile path 179 182 */ 180 - export function registerExtensionPath(id: string, fsPath: string): void { 181 - extensionPaths.set(id, fsPath); 182 - DEBUG && console.log('Registered extension path:', id, fsPath); 183 + export function registerTilePath(id: string, fsPath: string): void { 184 + tilePaths.set(id, fsPath); 185 + DEBUG && console.log('Registered tile path:', id, fsPath); 183 186 } 184 187 185 188 /** 186 - * Get all registered built-in extension IDs 189 + * Get all registered built-in tile IDs 187 190 */ 188 - export function getRegisteredExtensionIds(): string[] { 189 - return Array.from(extensionPaths.keys()); 191 + export function getRegisteredTileIds(): string[] { 192 + return Array.from(tilePaths.keys()); 190 193 } 191 194 192 195 /** ··· 239 242 } 240 243 241 244 /** 242 - * Get extension filesystem path by ID 243 - * First checks built-in extensions, then datastore for external extensions 245 + * Get tile filesystem path by ID. 246 + * First checks built-in tiles, then the datastore for installed (external) tiles. 244 247 */ 245 - export function getExtensionPath(id: string): string | null { 246 - // Check built-in extensions first 247 - const builtinPath = extensionPaths.get(id); 248 + export function getTilePath(id: string): string | null { 249 + // Check built-in tiles first 250 + const builtinPath = tilePaths.get(id); 248 251 if (builtinPath) return builtinPath; 249 252 250 - // Check datastore for external extensions 253 + // Check datastore for installed (external) tiles 251 254 try { 252 255 const db = getDb(); 253 256 const ext = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id) as { path?: string } | undefined; ··· 317 320 return fetchFile(absolutePath, req.url); 318 321 } 319 322 320 - // Handle extension content: peek://ext/{ext-id}/{path} 323 + // Handle tile content: peek://ext/{tile-id}/{path} 324 + // (The `ext/` URL prefix is a legacy shape; the resolver routes to tiles.) 321 325 if (host === 'ext') { 322 326 const parts = pathname.split('/'); 323 327 const extId = parts[0]; ··· 369 373 return fetchFile(absolutePath, req.url); 370 374 } 371 375 372 - const extBasePath = getExtensionPath(extId); 373 - if (!extBasePath) { 374 - DEBUG && console.log('Extension not found:', extId); 375 - return new Response('Extension not found', { status: 404 }); 376 + const tileBasePath = getTilePath(extId); 377 + if (!tileBasePath) { 378 + DEBUG && console.log('Tile not found:', extId); 379 + return new Response('Tile not found', { status: 404 }); 376 380 } 377 381 378 - const absolutePath = path.resolve(extBasePath, extPath); 382 + const absolutePath = path.resolve(tileBasePath, extPath); 379 383 380 - // Security: ensure path stays within extension folder 381 - const normalizedBase = path.normalize(extBasePath); 384 + // Security: ensure path stays within tile folder 385 + const normalizedBase = path.normalize(tileBasePath); 382 386 if (!absolutePath.startsWith(normalizedBase)) { 383 387 console.error('Path traversal attempt blocked:', absolutePath); 384 388 return new Response('Forbidden', { status: 403 }); ··· 389 393 return fetchFile(absolutePath, req.url); 390 394 } 391 395 392 - // Handle extensions infrastructure: peek://extensions/{path} 393 - // This serves the extension loader and other shared extension code 396 + // Handle shared tile infrastructure: peek://extensions/{path} 397 + // Resolves under features/ — used to serve cross-tile shared code. 394 398 if (host === 'extensions') { 395 399 const absolutePath = path.resolve(rootDir, 'features', pathname); 396 400 ··· 633 637 return fetchFile(absolutePath, req.url); 634 638 } 635 639 636 - // Handle per-extension hosts: peek://{ext-id}/{path} 637 - // This gives each extension a unique origin for iframe isolation 638 - // Check if host matches a registered extension 639 - const extBasePath = getExtensionPath(host); 640 - if (extBasePath) { 641 - const extPath = pathname || 'background.html'; 642 - const absolutePath = path.resolve(extBasePath, extPath); 640 + // Handle per-tile hosts: peek://{tile-id}/{path} 641 + // Each tile gets a unique origin for iframe isolation and CSP scoping. 642 + const tileBasePath = getTilePath(host); 643 + if (tileBasePath) { 644 + const tilePathSuffix = pathname || 'background.html'; 645 + const absolutePath = path.resolve(tileBasePath, tilePathSuffix); 643 646 644 647 // A v2 tile origin is identified by having either a loaded manifest 645 648 // (eager tiles) or a registered lazy manifest (not yet instantiated). 646 - // V1 extensions share the same per-extension host path but are not 647 - // tiles — we do not inject a tile CSP for them. 648 649 const tileManifest = getTileManifest(host); 649 650 const isTileOrigin = tileManifest !== null || isLazyTileRegistered(host); 650 651 651 - // Security: ensure path stays within extension folder. For tile origins 652 + // Security: ensure path stays within the tile folder. For tile origins 652 653 // we still want CSP on the 403, so we synthesize the forbidden response 653 654 // and fall through to the CSP-injection tail instead of returning early. 654 - const normalizedBase = path.normalize(extBasePath); 655 + const normalizedBase = path.normalize(tileBasePath); 655 656 let response: Response; 656 657 if (!absolutePath.startsWith(normalizedBase)) { 657 658 console.error('Path traversal attempt blocked:', absolutePath);
+6 -6
backend/electron/session.ts
··· 17 17 import { getDb, getContextEntry, addContextEntry } from './datastore.js'; 18 18 import { getAllWindows, getBackgroundWindow, pushClosedWindow, suppressClosedWindowPush } from './main.js'; 19 19 import type { ClosedWindowEntry } from './main.js'; 20 - import { getRegisteredExtensionIds } from './protocol.js'; 20 + import { getRegisteredTileIds } from './protocol.js'; 21 21 22 22 import { publish, getSystemAddress } from './pubsub.js'; 23 23 import { DEBUG, isTestProfile } from './config.js'; ··· 189 189 // Extract real URL from page containers 190 190 const realUrl = extractRealUrl(url); 191 191 192 - // Skip windows for unknown extensions (stale from renames/removals) 192 + // Skip windows for unknown tiles (stale from renames/removals) 193 193 const extMatch = realUrl.match(/^peek:\/\/ext\/([^/]+)/); 194 194 if (extMatch) { 195 195 const extId = extMatch[1]; 196 - const knownIds = getRegisteredExtensionIds(); 196 + const knownIds = getRegisteredTileIds(); 197 197 if (!knownIds.includes(extId)) { 198 - DEBUG && console.log(`[session:save] Skipping stale extension window ${id}: ${extId}`); 198 + DEBUG && console.log(`[session:save] Skipping stale tile window ${id}: ${extId}`); 199 199 continue; 200 200 } 201 201 } ··· 423 423 } 424 424 } catch { /* unparseable URL */ } 425 425 if (extIdFromUrl) { 426 - const knownIds = getRegisteredExtensionIds(); 426 + const knownIds = getRegisteredTileIds(); 427 427 if (!knownIds.includes(extIdFromUrl)) { 428 - DEBUG && console.log(`[session] Filtering stale extension window: ${extIdFromUrl}`); 428 + DEBUG && console.log(`[session] Filtering stale tile window: ${extIdFromUrl}`); 429 429 return false; 430 430 } 431 431 }
+2 -2
backend/electron/tile-compat.ts
··· 16 16 import { launchTile, type TileLaunchResult } from './tile-launcher.js'; 17 17 import { registerLazyTile } from './tile-lazy.js'; 18 18 import { registerTileIpcHandlers } from './tile-ipc.js'; 19 - import { registerExtensionPath } from './protocol.js'; 19 + import { registerTilePath } from './protocol.js'; 20 20 import { DEBUG } from './config.js'; 21 21 22 22 // ─── Types ─────────────────────────────────────────────────────────── ··· 132 132 } 133 133 134 134 // Register with protocol handler so peek://{tileId}/ URLs resolve 135 - registerExtensionPath(ext.id, ext.path); 135 + registerTilePath(ext.id, ext.path); 136 136 137 137 const manifest = ext.v2Manifest; 138 138 const isEager = config.eagerIds?.has(ext.id) ?? false;
+2 -2
backend/electron/tile-launcher.ts
··· 48 48 import { publish, getSystemAddress, unsubscribeAll } from './pubsub.js'; 49 49 import { DEBUG, getTilePreloadPath } from './config.js'; 50 50 import { loadSchemaDefaults } from './tile-settings-defaults.js'; 51 - import { getExtensionPath } from './protocol.js'; 51 + import { getTilePath } from './protocol.js'; 52 52 import * as tileLifecycle from './tile-lifecycle.js'; 53 53 54 54 // Config hooks for values sourced from electron-heavy modules ··· 800 800 * - `getTilePreloadPath()` not configured: returns null (logs error). 801 801 */ 802 802 export async function relaunchTile(tileId: string): Promise<TileLaunchResult | null> { 803 - const tilePath = getExtensionPath(tileId); 803 + const tilePath = getTilePath(tileId); 804 804 if (!tilePath) { 805 805 console.error(`[tile-launcher] relaunchTile: no path registered for tile ${tileId}`); 806 806 return null;