experiments in a post-browser web
10
fork

Configure Feed

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

feat(v1-removal): Phase 3.7f — migrate profiles to strict tile:profiles:* handlers

Move profiles:list / create / get / delete / getCurrent / switch /
enableSync / disableSync / getSyncConfig / getPartition from legacy
ipc.ts handlers to strict tile:profiles:* handlers in tile-ipc.ts.
Behavior preserved exactly (same app.relaunch on switch, same
session-partition lookup on getPartition).

- ipc.ts: drop legacy profiles:* handlers and now-unused imports
(entire profiles.js named-import block, Profile type)
- tile-ipc.ts: import all 9 profile helpers from profiles.js, add
10 tile:profiles:* handlers behind trustedBuiltin grant
- tile-preload.cts: api.profiles.* now invokes strict channels with
token (also fixes serverProfileId → serverProfileSlug arg name to
match the handler signature)

Fourth of 5 namespace migrations in Phase 3.7f. Build green.

+227 -177
+5 -159
backend/electron/ipc.ts
··· 35 35 loadExtensionManifest, 36 36 } from './extensions.js'; 37 37 38 - import type { Profile } from './profiles.js'; 39 - import { 40 - listProfiles, 41 - createProfile, 42 - getProfileByFolder, 43 - deleteProfile, 44 - getActiveProfile, 45 - setActiveProfile, 46 - enableSync, 47 - disableSync, 48 - getSyncConfig as getProfileSyncConfig, 49 - } from './profiles.js'; 50 38 51 39 import { 52 40 getExtensionPath, ··· 2595 2583 2596 2584 /** 2597 2585 * Register profile-related IPC handlers 2586 + * 2587 + * Strict tile:profiles:* counterparts live in tile-ipc.ts (Phase 3.7f). 2588 + * Legacy `profiles:*` ipcMain handlers were removed there too. Settings 2589 + * UI consumes these via api.profiles.*. 2598 2590 */ 2599 2591 export function registerProfileHandlers(): void { 2600 - // List all profiles 2601 - ipcMain.handle('profiles:list', async () => { 2602 - try { 2603 - const profiles = listProfiles(); 2604 - return { success: true, data: profiles }; 2605 - } catch (error) { 2606 - const message = error instanceof Error ? error.message : String(error); 2607 - return { success: false, error: message }; 2608 - } 2609 - }); 2610 - 2611 - // Create a new profile 2612 - ipcMain.handle('profiles:create', async (_ev, data: { name: string }) => { 2613 - try { 2614 - const profile = createProfile(data.name); 2615 - return { success: true, data: profile }; 2616 - } catch (error) { 2617 - const message = error instanceof Error ? error.message : String(error); 2618 - return { success: false, error: message }; 2619 - } 2620 - }); 2621 - 2622 - // Get a specific profile by slug 2623 - ipcMain.handle('profiles:get', async (_ev, data: { slug: string }) => { 2624 - try { 2625 - const profile = getProfileByFolder(data.slug); 2626 - if (!profile) { 2627 - return { success: false, error: 'Profile not found' }; 2628 - } 2629 - return { success: true, data: profile }; 2630 - } catch (error) { 2631 - const message = error instanceof Error ? error.message : String(error); 2632 - return { success: false, error: message }; 2633 - } 2634 - }); 2635 - 2636 - // Delete a profile 2637 - ipcMain.handle('profiles:delete', async (_ev, data: { id: string }) => { 2638 - try { 2639 - deleteProfile(data.id); 2640 - return { success: true }; 2641 - } catch (error) { 2642 - const message = error instanceof Error ? error.message : String(error); 2643 - return { success: false, error: message }; 2644 - } 2645 - }); 2646 - 2647 - // Get the currently active profile 2648 - ipcMain.handle('profiles:getCurrent', async () => { 2649 - try { 2650 - const profile = getActiveProfile(); 2651 - return { success: true, data: profile }; 2652 - } catch (error) { 2653 - const message = error instanceof Error ? error.message : String(error); 2654 - return { success: false, error: message }; 2655 - } 2656 - }); 2657 - 2658 - // Switch to a different profile (causes app restart) 2659 - ipcMain.handle('profiles:switch', async (_ev, data: { slug: string }) => { 2660 - try { 2661 - DEBUG && console.log(`[ipc:profiles] Switch requested to profile: ${data.slug}`); 2662 - 2663 - const currentProfile = getActiveProfile(); 2664 - DEBUG && console.log(`[ipc:profiles] Current profile: ${currentProfile.folder}`); 2665 - 2666 - // Check if already on this profile 2667 - if (currentProfile.folder === data.slug) { 2668 - DEBUG && console.log('[ipc:profiles] Already on requested profile, no-op'); 2669 - return { success: true, message: 'Already on this profile' }; 2670 - } 2671 - 2672 - const profile = getProfileByFolder(data.slug); 2673 - if (!profile) { 2674 - return { success: false, error: 'Profile not found' }; 2675 - } 2676 - 2677 - // Set as active profile 2678 - DEBUG && console.log(`[ipc:profiles] Setting active profile to: ${data.slug}`); 2679 - setActiveProfile(data.slug); 2680 - 2681 - DEBUG && console.log('[ipc:profiles] Relaunching app...'); 2682 - // Relaunch the app with the new profile 2683 - // The app will pick up the new active profile from profiles.db on restart 2684 - app.relaunch(); 2685 - app.quit(); 2686 - 2687 - return { success: true }; 2688 - } catch (error) { 2689 - const message = error instanceof Error ? error.message : String(error); 2690 - return { success: false, error: message }; 2691 - } 2692 - }); 2693 - 2694 - // Enable sync for a profile 2695 - ipcMain.handle('profiles:enableSync', async (_ev, data: { 2696 - profileId: string; 2697 - apiKey: string; 2698 - serverProfileSlug: string; 2699 - }) => { 2700 - try { 2701 - enableSync(data.profileId, data.apiKey, data.serverProfileSlug); 2702 - return { success: true }; 2703 - } catch (error) { 2704 - const message = error instanceof Error ? error.message : String(error); 2705 - return { success: false, error: message }; 2706 - } 2707 - }); 2708 - 2709 - // Disable sync for a profile 2710 - ipcMain.handle('profiles:disableSync', async (_ev, data: { profileId: string }) => { 2711 - try { 2712 - disableSync(data.profileId); 2713 - return { success: true }; 2714 - } catch (error) { 2715 - const message = error instanceof Error ? error.message : String(error); 2716 - return { success: false, error: message }; 2717 - } 2718 - }); 2719 - 2720 - // Get sync configuration for a profile 2721 - ipcMain.handle('profiles:getSyncConfig', async (_ev, data: { profileId: string }) => { 2722 - try { 2723 - const syncConfig = getProfileSyncConfig(data.profileId); 2724 - return { success: true, data: syncConfig }; 2725 - } catch (error) { 2726 - const message = error instanceof Error ? error.message : String(error); 2727 - return { success: false, error: message }; 2728 - } 2729 - }); 2730 - 2731 - // Get the partition string for the current profile's session 2732 - // Used by renderers to set webview partition attribute 2733 - ipcMain.handle('profiles:getPartition', async () => { 2734 - try { 2735 - const { getPartitionString, getCurrentProfileId } = await import('./session-partition.js'); 2736 - const profileId = getCurrentProfileId(); 2737 - if (!profileId) { 2738 - return { success: false, error: 'No profile session initialized' }; 2739 - } 2740 - const partition = getPartitionString(profileId); 2741 - return { success: true, data: { profileId, partition } }; 2742 - } catch (error) { 2743 - const message = error instanceof Error ? error.message : String(error); 2744 - return { success: false, error: message }; 2745 - } 2746 - }); 2592 + // No-op — see tile-ipc.ts for the strict tile:profiles:* handlers. 2747 2593 } 2748 2594 2749 2595 /**
+207 -1
backend/electron/tile-ipc.ts
··· 110 110 import { checkSyncAllowed } from './tile-sync-enforcement.js'; 111 111 import { createLoopbackServer } from './oauth-loopback.js'; 112 112 import { syncAll, getSyncConfig, setSyncConfig, getSyncStatus, pullFromServer, pushToServer } from './sync.js'; 113 - import { getActiveProfile, updateLastSyncTime } from './profiles.js'; 113 + import { 114 + listProfiles, 115 + createProfile, 116 + getProfileByFolder, 117 + deleteProfile, 118 + getActiveProfile, 119 + setActiveProfile, 120 + enableSync, 121 + disableSync, 122 + getSyncConfig as getProfileSyncConfig, 123 + updateLastSyncTime, 124 + } from './profiles.js'; 114 125 import { checkEscapeAllowed } from './tile-escape-enforcement.js'; 115 126 import { checkSessionAllowed } from './tile-session-enforcement.js'; 116 127 import { checkIzuiAllowed } from './tile-izui-enforcement.js'; ··· 4428 4439 } catch (error) { 4429 4440 const message = error instanceof Error ? error.message : String(error); 4430 4441 return { success: false, error: message }; 4442 + } 4443 + }); 4444 + 4445 + // ── tile:profiles:* (Phase 3.7f) ────────────────────────────────── 4446 + // 4447 + // Strict counterparts of the legacy `profiles:*` channels in ipc.ts 4448 + // (deleted in Phase 3.7f). Settings UI consumes via api.profiles. 4449 + // trustedBuiltin only — admin surface (touches profiles.db, can 4450 + // relaunch app, holds sync creds). 4451 + 4452 + registerTileIpc('tile:profiles:list', { mode: 'handle' }, (event, args: { 4453 + token: string; 4454 + }, _grant) => { 4455 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4456 + const grant = _grant; 4457 + if (!grant.trustedBuiltin) { 4458 + handleViolation(grant, 'profiles', 'tile:profiles:list', 'trustedBuiltin required', args.token); 4459 + return { success: false, error: 'trustedBuiltin required for tile:profiles:list' }; 4460 + } 4461 + try { 4462 + return { success: true, data: listProfiles() }; 4463 + } catch (error) { 4464 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4465 + } 4466 + }); 4467 + 4468 + registerTileIpc('tile:profiles:create', { mode: 'handle' }, (event, args: { 4469 + token: string; 4470 + name: string; 4471 + }, _grant) => { 4472 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4473 + const grant = _grant; 4474 + if (!grant.trustedBuiltin) { 4475 + handleViolation(grant, 'profiles', 'tile:profiles:create', 'trustedBuiltin required', args.token); 4476 + return { success: false, error: 'trustedBuiltin required for tile:profiles:create' }; 4477 + } 4478 + try { 4479 + return { success: true, data: createProfile(args.name) }; 4480 + } catch (error) { 4481 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4482 + } 4483 + }); 4484 + 4485 + registerTileIpc('tile:profiles:get', { mode: 'handle' }, (event, args: { 4486 + token: string; 4487 + slug: string; 4488 + }, _grant) => { 4489 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4490 + const grant = _grant; 4491 + if (!grant.trustedBuiltin) { 4492 + handleViolation(grant, 'profiles', 'tile:profiles:get', 'trustedBuiltin required', args.token); 4493 + return { success: false, error: 'trustedBuiltin required for tile:profiles:get' }; 4494 + } 4495 + try { 4496 + const profile = getProfileByFolder(args.slug); 4497 + if (!profile) return { success: false, error: 'Profile not found' }; 4498 + return { success: true, data: profile }; 4499 + } catch (error) { 4500 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4501 + } 4502 + }); 4503 + 4504 + registerTileIpc('tile:profiles:delete', { mode: 'handle' }, (event, args: { 4505 + token: string; 4506 + id: string; 4507 + }, _grant) => { 4508 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4509 + const grant = _grant; 4510 + if (!grant.trustedBuiltin) { 4511 + handleViolation(grant, 'profiles', 'tile:profiles:delete', 'trustedBuiltin required', args.token); 4512 + return { success: false, error: 'trustedBuiltin required for tile:profiles:delete' }; 4513 + } 4514 + try { 4515 + deleteProfile(args.id); 4516 + return { success: true }; 4517 + } catch (error) { 4518 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4519 + } 4520 + }); 4521 + 4522 + registerTileIpc('tile:profiles:getCurrent', { mode: 'handle' }, (event, args: { 4523 + token: string; 4524 + }, _grant) => { 4525 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4526 + const grant = _grant; 4527 + if (!grant.trustedBuiltin) { 4528 + handleViolation(grant, 'profiles', 'tile:profiles:getCurrent', 'trustedBuiltin required', args.token); 4529 + return { success: false, error: 'trustedBuiltin required for tile:profiles:getCurrent' }; 4530 + } 4531 + try { 4532 + return { success: true, data: getActiveProfile() }; 4533 + } catch (error) { 4534 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4535 + } 4536 + }); 4537 + 4538 + registerTileIpc('tile:profiles:switch', { mode: 'handle' }, (event, args: { 4539 + token: string; 4540 + slug: string; 4541 + }, _grant) => { 4542 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4543 + const grant = _grant; 4544 + if (!grant.trustedBuiltin) { 4545 + handleViolation(grant, 'profiles', 'tile:profiles:switch', 'trustedBuiltin required', args.token); 4546 + return { success: false, error: 'trustedBuiltin required for tile:profiles:switch' }; 4547 + } 4548 + try { 4549 + DEBUG && console.log(`[tile:profiles] Switch requested to profile: ${args.slug}`); 4550 + const currentProfile = getActiveProfile(); 4551 + if (currentProfile.folder === args.slug) { 4552 + return { success: true, message: 'Already on this profile' }; 4553 + } 4554 + const profile = getProfileByFolder(args.slug); 4555 + if (!profile) return { success: false, error: 'Profile not found' }; 4556 + setActiveProfile(args.slug); 4557 + app.relaunch(); 4558 + app.quit(); 4559 + return { success: true }; 4560 + } catch (error) { 4561 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4562 + } 4563 + }); 4564 + 4565 + registerTileIpc('tile:profiles:enableSync', { mode: 'handle' }, (event, args: { 4566 + token: string; 4567 + profileId: string; 4568 + apiKey: string; 4569 + serverProfileSlug: string; 4570 + }, _grant) => { 4571 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4572 + const grant = _grant; 4573 + if (!grant.trustedBuiltin) { 4574 + handleViolation(grant, 'profiles', 'tile:profiles:enableSync', 'trustedBuiltin required', args.token); 4575 + return { success: false, error: 'trustedBuiltin required for tile:profiles:enableSync' }; 4576 + } 4577 + try { 4578 + enableSync(args.profileId, args.apiKey, args.serverProfileSlug); 4579 + return { success: true }; 4580 + } catch (error) { 4581 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4582 + } 4583 + }); 4584 + 4585 + registerTileIpc('tile:profiles:disableSync', { mode: 'handle' }, (event, args: { 4586 + token: string; 4587 + profileId: string; 4588 + }, _grant) => { 4589 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4590 + const grant = _grant; 4591 + if (!grant.trustedBuiltin) { 4592 + handleViolation(grant, 'profiles', 'tile:profiles:disableSync', 'trustedBuiltin required', args.token); 4593 + return { success: false, error: 'trustedBuiltin required for tile:profiles:disableSync' }; 4594 + } 4595 + try { 4596 + disableSync(args.profileId); 4597 + return { success: true }; 4598 + } catch (error) { 4599 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4600 + } 4601 + }); 4602 + 4603 + registerTileIpc('tile:profiles:getSyncConfig', { mode: 'handle' }, (event, args: { 4604 + token: string; 4605 + profileId: string; 4606 + }, _grant) => { 4607 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4608 + const grant = _grant; 4609 + if (!grant.trustedBuiltin) { 4610 + handleViolation(grant, 'profiles', 'tile:profiles:getSyncConfig', 'trustedBuiltin required', args.token); 4611 + return { success: false, error: 'trustedBuiltin required for tile:profiles:getSyncConfig' }; 4612 + } 4613 + try { 4614 + return { success: true, data: getProfileSyncConfig(args.profileId) }; 4615 + } catch (error) { 4616 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4617 + } 4618 + }); 4619 + 4620 + registerTileIpc('tile:profiles:getPartition', { mode: 'handle' }, async (event, args: { 4621 + token: string; 4622 + }, _grant) => { 4623 + if (!args?.token) return { success: false, error: 'Invalid token' }; 4624 + const grant = _grant; 4625 + if (!grant.trustedBuiltin) { 4626 + handleViolation(grant, 'profiles', 'tile:profiles:getPartition', 'trustedBuiltin required', args.token); 4627 + return { success: false, error: 'trustedBuiltin required for tile:profiles:getPartition' }; 4628 + } 4629 + try { 4630 + const { getPartitionString, getCurrentProfileId } = await import('./session-partition.js'); 4631 + const profileId = getCurrentProfileId(); 4632 + if (!profileId) return { success: false, error: 'No profile session initialized' }; 4633 + const partition = getPartitionString(profileId); 4634 + return { success: true, data: { profileId, partition } }; 4635 + } catch (error) { 4636 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4431 4637 } 4432 4638 }); 4433 4639
+15 -17
backend/electron/tile-preload.cts
··· 176 176 177 177 // ── Profiles (admin surface) ── 178 178 // 179 - // Thin wrappers over the existing `profiles:*` ipcMain handlers in 180 - // ipc.ts. No capability gate — Settings is the only real consumer 181 - // and it has a trustedBuiltin grant. The whole namespace went 182 - // missing during the v1→v2 migration; without it, settings.js 183 - // calls (`api.profiles.list()`, etc.) throw before the try/catch 184 - // and the Profiles section renders empty. 179 + // Strict tile:profiles:* counterparts of the legacy `profiles:*` 180 + // handlers live in tile-ipc.ts (Phase 3.7f). trustedBuiltin only 181 + // — touches profiles.db, can relaunch the app, holds sync creds. 182 + // Settings UI is the only real consumer. 185 183 api.profiles = { 186 - list: () => ipcRenderer.invoke('profiles:list'), 187 - create: (name: string) => ipcRenderer.invoke('profiles:create', { name }), 188 - get: (slug: string) => ipcRenderer.invoke('profiles:get', { slug }), 189 - delete: (id: string) => ipcRenderer.invoke('profiles:delete', { id }), 190 - getCurrent: () => ipcRenderer.invoke('profiles:getCurrent'), 191 - switch: (slug: string) => ipcRenderer.invoke('profiles:switch', { slug }), 192 - enableSync: (profileId: string, apiKey: string, serverProfileId: string) => 193 - ipcRenderer.invoke('profiles:enableSync', { profileId, apiKey, serverProfileId }), 184 + list: () => ipcRenderer.invoke('tile:profiles:list', { token: tileToken }), 185 + create: (name: string) => ipcRenderer.invoke('tile:profiles:create', { token: tileToken, name }), 186 + get: (slug: string) => ipcRenderer.invoke('tile:profiles:get', { token: tileToken, slug }), 187 + delete: (id: string) => ipcRenderer.invoke('tile:profiles:delete', { token: tileToken, id }), 188 + getCurrent: () => ipcRenderer.invoke('tile:profiles:getCurrent', { token: tileToken }), 189 + switch: (slug: string) => ipcRenderer.invoke('tile:profiles:switch', { token: tileToken, slug }), 190 + enableSync: (profileId: string, apiKey: string, serverProfileSlug: string) => 191 + ipcRenderer.invoke('tile:profiles:enableSync', { token: tileToken, profileId, apiKey, serverProfileSlug }), 194 192 disableSync: (profileId: string) => 195 - ipcRenderer.invoke('profiles:disableSync', { profileId }), 193 + ipcRenderer.invoke('tile:profiles:disableSync', { token: tileToken, profileId }), 196 194 getSyncConfig: (profileId: string) => 197 - ipcRenderer.invoke('profiles:getSyncConfig', { profileId }), 198 - getPartition: () => ipcRenderer.invoke('profiles:getPartition'), 195 + ipcRenderer.invoke('tile:profiles:getSyncConfig', { token: tileToken, profileId }), 196 + getPartition: () => ipcRenderer.invoke('tile:profiles:getPartition', { token: tileToken }), 199 197 }; 200 198 201 199 // ── PubSub (if granted) ──