experiments in a post-browser web
10
fork

Configure Feed

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

refactor(tile-preload,ipc): flip extension-* to strict + delete legacy (Phase 3.6c)

- tile-preload: all api.extensions.* methods now invoke tile:extensions:*
channels directly (trustedBuiltin guard preserved, token passed through)
- tile-preload: deleted _strict namespace added in 3.5c — public methods
ARE the strict implementation now, no separate namespace needed
- ipc.ts: deleted 10 legacy extension-* ipcMain handlers (extension-pick-folder,
extension-validate-folder, extension-add, extension-remove, extension-update,
extension-get-all, extension-get, extension-window-list,
extension-list-all-registered, extension-window-devtools, extension-reload)
- No callers of ipcRenderer.invoke('extension-*') remain in tile-preload.cts
- No ipcMain.handle('extension-*') survivors except extension-window-* kept
intentionally (different concern, not in scope)

+23 -322
+8 -248
backend/electron/ipc.ts
··· 1082 1082 1083 1083 /** 1084 1084 * Register extension IPC handlers 1085 + * 1086 + * Note (Phase 3.6c): The 10 legacy extension-* handlers 1087 + * (extension-pick-folder, extension-validate-folder, extension-add, 1088 + * extension-remove, extension-update, extension-get-all, extension-get, 1089 + * extension-window-list, extension-list-all-registered, 1090 + * extension-window-devtools, extension-reload) have been deleted. 1091 + * tile-preload now routes all api.extensions.* calls through the strict 1092 + * tile:extensions:* channels registered in tile-ipc.ts. 1085 1093 */ 1086 1094 export function registerExtensionHandlers(): void { 1087 - ipcMain.handle('extension-pick-folder', async (ev) => { 1088 - try { 1089 - const result = await dialog.showOpenDialog({ 1090 - properties: ['openDirectory'] 1091 - }); 1092 - if (result.canceled || result.filePaths.length === 0) { 1093 - return { success: false, error: 'No folder selected' }; 1094 - } 1095 - return { success: true, data: { path: result.filePaths[0] } }; 1096 - } catch (error) { 1097 - const message = error instanceof Error ? error.message : String(error); 1098 - return { success: false, error: message }; 1099 - } 1100 - }); 1101 - 1102 - ipcMain.handle('extension-validate-folder', async (ev, data) => { 1103 - try { 1104 - const extPath = data.folderPath || data.path; 1105 - const manifestPath = path.join(extPath, 'manifest.json'); 1106 - 1107 - if (!fs.existsSync(manifestPath)) { 1108 - return { success: false, error: 'No manifest.json found in folder' }; 1109 - } 1110 - 1111 - const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); 1112 - const manifest = JSON.parse(manifestContent); 1113 - 1114 - if (!manifest.id && !manifest.shortname && !manifest.name) { 1115 - return { success: false, error: 'Manifest must have id, shortname, or name' }; 1116 - } 1117 - 1118 - // Check for background.html 1119 - const backgroundPath = path.join(extPath, 'background.html'); 1120 - if (!fs.existsSync(backgroundPath)) { 1121 - return { success: false, error: 'No background.html found in folder' }; 1122 - } 1123 - 1124 - return { 1125 - success: true, 1126 - data: { 1127 - manifest, 1128 - path: extPath 1129 - } 1130 - }; 1131 - } catch (error) { 1132 - const message = error instanceof Error ? error.message : String(error); 1133 - return { success: false, error: message }; 1134 - } 1135 - }); 1136 - 1137 - ipcMain.handle('extension-add', async (ev, data) => { 1138 - try { 1139 - const db = getDb(); 1140 - const extPath = data.folderPath || data.path; 1141 - const manifestPath = path.join(extPath, 'manifest.json'); 1142 - 1143 - if (!fs.existsSync(manifestPath)) { 1144 - return { success: false, error: 'No manifest.json found' }; 1145 - } 1146 - 1147 - const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); 1148 - const manifest = JSON.parse(manifestContent); 1149 - const id = manifest.id || manifest.shortname || manifest.name || `ext_${Date.now()}`; 1150 - 1151 - // Check if already exists 1152 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 1153 - if (existing) { 1154 - return { success: false, error: `Extension ${id} already installed` }; 1155 - } 1156 - 1157 - // Get lastError if provided 1158 - const lastError = data.lastError || ''; 1159 - const lastErrorAt = lastError ? Date.now() : 0; 1160 - 1161 - // Insert into database 1162 - db.prepare(` 1163 - INSERT INTO extensions (id, name, description, version, path, enabled, builtin, status, installedAt, updatedAt, metadata, lastError, lastErrorAt) 1164 - VALUES (?, ?, ?, ?, ?, 1, 0, 'installed', ?, ?, ?, ?, ?) 1165 - `).run( 1166 - id, 1167 - manifest.name || id, 1168 - manifest.description || '', 1169 - manifest.version || '0.0.0', 1170 - extPath, 1171 - Date.now(), 1172 - Date.now(), 1173 - JSON.stringify(manifest), 1174 - lastError, 1175 - lastErrorAt 1176 - ); 1177 - 1178 - return { success: true, data: { id, manifest, path: extPath, lastError: lastError || null } }; 1179 - } catch (error) { 1180 - const message = error instanceof Error ? error.message : String(error); 1181 - return { success: false, error: message }; 1182 - } 1183 - }); 1184 - 1185 - ipcMain.handle('extension-remove', async (ev, data) => { 1186 - try { 1187 - const db = getDb(); 1188 - const extId = data.id; 1189 - 1190 - // Check if exists 1191 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 1192 - if (!existing) { 1193 - return { success: false, error: `Extension ${extId} not found` }; 1194 - } 1195 - 1196 - // Remove from database 1197 - db.prepare('DELETE FROM extensions WHERE id = ?').run(extId); 1198 - db.prepare('DELETE FROM feature_settings WHERE featureId = ?').run(extId); 1199 - 1200 - return { success: true, data: { id: extId } }; 1201 - } catch (error) { 1202 - const message = error instanceof Error ? error.message : String(error); 1203 - return { success: false, error: message }; 1204 - } 1205 - }); 1206 - 1207 - ipcMain.handle('extension-update', async (ev, data) => { 1208 - try { 1209 - const db = getDb(); 1210 - const extId = data.id; 1211 - const updates = data.updates || {}; 1212 - 1213 - const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 1214 - if (!existing) { 1215 - return { success: false, error: `Extension ${extId} not found` }; 1216 - } 1217 - 1218 - const fields: string[] = []; 1219 - const values: unknown[] = []; 1220 - 1221 - if (updates.enabled !== undefined) { 1222 - fields.push('enabled = ?'); 1223 - values.push(updates.enabled ? 1 : 0); 1224 - } 1225 - if (updates.status !== undefined) { 1226 - fields.push('status = ?'); 1227 - values.push(updates.status); 1228 - } 1229 - if (updates.lastError !== undefined) { 1230 - fields.push('lastError = ?'); 1231 - values.push(updates.lastError); 1232 - } 1233 - if (updates.lastErrorAt !== undefined) { 1234 - fields.push('lastErrorAt = ?'); 1235 - values.push(updates.lastErrorAt); 1236 - } 1237 - 1238 - if (fields.length > 0) { 1239 - fields.push('updatedAt = ?'); 1240 - values.push(Date.now()); 1241 - values.push(extId); 1242 - db.prepare(`UPDATE extensions SET ${fields.join(', ')} WHERE id = ?`).run(...values); 1243 - } 1244 - 1245 - const updated = db.prepare('SELECT * FROM extensions WHERE id = ?').get(extId); 1246 - return { success: true, data: updated }; 1247 - } catch (error) { 1248 - const message = error instanceof Error ? error.message : String(error); 1249 - return { success: false, error: message }; 1250 - } 1251 - }); 1252 - 1253 - ipcMain.handle('extension-get-all', async () => { 1254 - try { 1255 - const db = getDb(); 1256 - const extensions = db.prepare('SELECT * FROM extensions').all(); 1257 - return { success: true, data: extensions }; 1258 - } catch (error) { 1259 - const message = error instanceof Error ? error.message : String(error); 1260 - return { success: false, error: message }; 1261 - } 1262 - }); 1263 - 1264 - ipcMain.handle('extension-get', async (ev, data) => { 1265 - try { 1266 - const db = getDb(); 1267 - const ext = db.prepare('SELECT * FROM extensions WHERE id = ?').get(data.id); 1268 - if (!ext) { 1269 - return { success: false, error: `Extension ${data.id} not found` }; 1270 - } 1271 - return { success: true, data: ext }; 1272 - } catch (error) { 1273 - const message = error instanceof Error ? error.message : String(error); 1274 - return { success: false, error: message }; 1275 - } 1276 - }); 1277 - 1278 - // Note: `extension-window-load` and `extension-window-reload` were removed 1279 - // Note: `extension-window-load`, `extension-window-reload`, and 1280 - // `extension-window-unload` were removed in Phase 3.2 (dead handlers — 1281 - // extensionWindows map was always empty after Phase 2.5 #3). 1282 - // Extension reload now flows through `extension-reload` below. 1283 - ipcMain.handle('extension-window-list', async () => { 1284 - try { 1285 - const running = getRunningExtensions(); 1286 - return { success: true, data: running }; 1287 - } catch (error) { 1288 - const message = error instanceof Error ? error.message : String(error); 1289 - return { success: false, error: message }; 1290 - } 1291 - }); 1292 - 1293 - ipcMain.handle('extension-list-all-registered', async () => { 1294 - try { 1295 - const all = getAllRegisteredExtensions(); 1296 - return { success: true, data: all }; 1297 - } catch (error) { 1298 - const message = error instanceof Error ? error.message : String(error); 1299 - return { success: false, error: message }; 1300 - } 1301 - }); 1302 - 1303 - ipcMain.handle('extension-window-devtools', async (ev, data) => { 1304 - try { 1305 - const extId = data.id; 1306 - 1307 - // v2 tile or unknown — extensionWindows map removed in Phase 3.2 1308 - return { success: false, error: `Extension ${extId} is not running as a legacy window` }; 1309 - } catch (error) { 1310 - const message = error instanceof Error ? error.message : String(error); 1311 - return { success: false, error: message }; 1312 - } 1313 - }); 1314 - 1315 - // Reload an extension (destroy window and recreate) 1316 - ipcMain.handle('extension-reload', async (ev, data) => { 1317 - try { 1318 - const extId = data.id; 1319 - if (!extId) { 1320 - return { success: false, error: 'Missing extension id' }; 1321 - } 1322 - 1323 - const win = await reloadExtension(extId); 1324 - if (!win) { 1325 - return { success: false, error: `Failed to reload extension: ${extId}` }; 1326 - } 1327 - 1328 - return { success: true, data: { id: extId } }; 1329 - } catch (error) { 1330 - const message = error instanceof Error ? error.message : String(error); 1331 - return { success: false, error: message }; 1332 - } 1333 - }); 1334 - 1335 1095 // Feature settings handlers (renamed from extension-settings) 1336 1096 // Note: preload sends { extId } but we accept both extId and id for compatibility 1337 1097 ipcMain.handle('feature-settings-get', async (ev, data) => {
+15 -74
backend/electron/tile-preload.cts
··· 1978 1978 return ipcRenderer.invoke(channel, data); 1979 1979 }; 1980 1980 1981 - // ── Extensions management (test-fixture bgWindow compat) ───────────── 1981 + // ── Extensions management (Phase 3.6c) ──────────────────────────────── 1982 1982 // 1983 - // The v1 preload.js exposed an `api.extensions` surface used by: 1984 - // - The test fixture (Playwright tests call `api.extensions.list()`, 1985 - // `api.extensions.reload()`, `api.extensions.add()`, etc.) 1986 - // - Core admin UIs (features-manager, etc.) 1987 - // 1988 - // Each method is a thin wrapper over a named IPC channel in ipc.ts 1989 - // (`extension-window-list`, `extension-reload`, ...). Strict `tile:*` 1990 - // equivalents are available via `api.extensions._strict` (Phase 3.5c). 1991 - // Wave 3.6c will flip each method here to call those strict channels. 1983 + // All methods now route through tile:extensions:* strict channels in 1984 + // tile-ipc.ts (trustedBuiltin enforcement + capability-token validation). 1985 + // Legacy extension-* IPC handlers deleted from ipc.ts in this phase. 1992 1986 // 1993 1987 // Gated on trustedBuiltin — feature tiles must not be able to manage 1994 1988 // other extensions through the tile surface. ··· 1997 1991 if (!trustedBuiltin) { 1998 1992 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 1999 1993 } 2000 - return ipcRenderer.invoke('extension-window-list'); 1994 + return ipcRenderer.invoke('tile:extensions:windowList', { token: tileToken }); 2001 1995 }, 2002 1996 listAllRegistered: () => { 2003 1997 if (!trustedBuiltin) { 2004 1998 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2005 1999 } 2006 - return ipcRenderer.invoke('extension-list-all-registered'); 2000 + return ipcRenderer.invoke('tile:extensions:listAllRegistered', { token: tileToken }); 2007 2001 }, 2008 2002 reload: (id: string) => { 2009 2003 if (!trustedBuiltin) { 2010 2004 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2011 2005 } 2012 - return ipcRenderer.invoke('extension-reload', { id }); 2006 + return ipcRenderer.invoke('tile:extensions:reload', { token: tileToken, id }); 2013 2007 }, 2014 2008 devtools: (id: string) => { 2015 2009 if (!trustedBuiltin) { 2016 2010 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2017 2011 } 2018 - return ipcRenderer.invoke('extension-window-devtools', { id }); 2012 + return ipcRenderer.invoke('tile:extensions:windowDevtools', { token: tileToken, id }); 2019 2013 }, 2020 2014 pickFolder: () => { 2021 2015 if (!trustedBuiltin) { 2022 2016 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2023 2017 } 2024 - return ipcRenderer.invoke('extension-pick-folder'); 2018 + return ipcRenderer.invoke('tile:extensions:pickFolder', { token: tileToken }); 2025 2019 }, 2026 2020 validateFolder: (folderPath: string) => { 2027 2021 if (!trustedBuiltin) { 2028 2022 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2029 2023 } 2030 - return ipcRenderer.invoke('extension-validate-folder', { folderPath }); 2024 + return ipcRenderer.invoke('tile:extensions:validateFolder', { token: tileToken, folderPath }); 2031 2025 }, 2032 2026 add: (folderPath: string, manifest: unknown, enabled: boolean = false) => { 2033 2027 if (!trustedBuiltin) { 2034 2028 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2035 2029 } 2036 - return ipcRenderer.invoke('extension-add', { folderPath, manifest, enabled }); 2030 + return ipcRenderer.invoke('tile:extensions:add', { token: tileToken, folderPath, manifest, enabled }); 2037 2031 }, 2038 2032 remove: (id: string) => { 2039 2033 if (!trustedBuiltin) { 2040 2034 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2041 2035 } 2042 - return ipcRenderer.invoke('extension-remove', { id }); 2036 + return ipcRenderer.invoke('tile:extensions:remove', { token: tileToken, id }); 2043 2037 }, 2044 2038 update: (id: string, updates: unknown) => { 2045 2039 if (!trustedBuiltin) { 2046 2040 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2047 2041 } 2048 - return ipcRenderer.invoke('extension-update', { id, updates }); 2042 + return ipcRenderer.invoke('tile:extensions:update', { token: tileToken, id, updates }); 2049 2043 }, 2050 2044 getAll: () => { 2051 2045 if (!trustedBuiltin) { 2052 2046 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2053 2047 } 2054 - return ipcRenderer.invoke('extension-get-all'); 2048 + return ipcRenderer.invoke('tile:extensions:getAll', { token: tileToken }); 2055 2049 }, 2056 2050 get: (id: string) => { 2057 2051 if (!trustedBuiltin) { 2058 2052 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2059 2053 } 2060 - return ipcRenderer.invoke('extension-get', { id }); 2054 + return ipcRenderer.invoke('tile:extensions:get', { token: tileToken, id }); 2061 2055 }, 2062 2056 getSettingsSchema: (extId: string) => { 2063 2057 if (!trustedBuiltin) { 2064 2058 return Promise.resolve({ success: false, error: 'api.extensions requires trustedBuiltin' }); 2065 2059 } 2066 2060 return ipcRenderer.invoke('feature-settings-schema', { extId }); 2067 - }, 2068 - }; 2069 - 2070 - // ── Extensions strict shims (Phase 3.5c) ───────────────────────────── 2071 - // 2072 - // Strict counterparts that route through the `tile:extensions:*` channels 2073 - // in tile-ipc.ts with trustedBuiltin enforcement and capability-token 2074 - // validation. Wave 3.6c will flip `api.extensions.*` above to call these 2075 - // and remove the legacy `extension-*` invocations. 2076 - (api.extensions as Record<string, unknown>)._strict = { 2077 - pickFolder: () => { 2078 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2079 - return ipcRenderer.invoke('tile:extensions:pickFolder', { token: tileToken }); 2080 - }, 2081 - validateFolder: (folderPath: string) => { 2082 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2083 - return ipcRenderer.invoke('tile:extensions:validateFolder', { token: tileToken, folderPath }); 2084 - }, 2085 - add: (folderPath: string, manifest: unknown, enabled: boolean = false) => { 2086 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2087 - return ipcRenderer.invoke('tile:extensions:add', { token: tileToken, folderPath, manifest, enabled }); 2088 - }, 2089 - remove: (id: string) => { 2090 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2091 - return ipcRenderer.invoke('tile:extensions:remove', { token: tileToken, id }); 2092 - }, 2093 - update: (id: string, updates: unknown) => { 2094 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2095 - return ipcRenderer.invoke('tile:extensions:update', { token: tileToken, id, updates }); 2096 - }, 2097 - getAll: () => { 2098 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2099 - return ipcRenderer.invoke('tile:extensions:getAll', { token: tileToken }); 2100 - }, 2101 - get: (id: string) => { 2102 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2103 - return ipcRenderer.invoke('tile:extensions:get', { token: tileToken, id }); 2104 - }, 2105 - windowList: () => { 2106 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2107 - return ipcRenderer.invoke('tile:extensions:windowList', { token: tileToken }); 2108 - }, 2109 - listAllRegistered: () => { 2110 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2111 - return ipcRenderer.invoke('tile:extensions:listAllRegistered', { token: tileToken }); 2112 - }, 2113 - windowDevtools: (id: string) => { 2114 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2115 - return ipcRenderer.invoke('tile:extensions:windowDevtools', { token: tileToken, id }); 2116 - }, 2117 - reload: (id: string) => { 2118 - if (!tokenValid) return Promise.reject(new Error('Not initialized')); 2119 - return ipcRenderer.invoke('tile:extensions:reload', { token: tileToken, id }); 2120 2061 }, 2121 2062 }; 2122 2063