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
6 changes: 6 additions & 0 deletions CURRENT-SPRINT.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,9 @@ Reduce risk in privileged runtime boundaries by completing the P0 refactor set w

- 2026-02-13: Sprint initialized from governance backlog with P0 focus (`BL-016`, `BL-017`, `BL-018`).
- 2026-02-13: Added PR-sized execution breakdown with per-unit proof commands.
- 2026-02-13: Completed `BL-016B` by extracting desktop-main IPC handlers into per-domain modules under `apps/desktop-main/src/ipc/*`.
- 2026-02-13: Completed `BL-018A` and `BL-018B` by introducing `registerValidatedHandler` and migrating all privileged handlers to the shared authorization/validation wrapper.
- 2026-02-13: Completed `BL-017A` by extracting preload invoke/correlation/error-mapping core into `apps/desktop-preload/src/invoke-client.ts`.
- 2026-02-13: Completed `BL-017B` by splitting preload app/auth/dialog/fs/storage/external/updates/telemetry APIs into `apps/desktop-preload/src/api/*` and reducing `apps/desktop-preload/src/main.ts` to composition-only wiring.
- 2026-02-13: Verification pass after `BL-017B` completed: `pnpm nx run desktop-preload:build`, `pnpm nx run desktop-main:build`, `pnpm nx run desktop-main:test`, and `pnpm nx run renderer:build` all passed.
- 2026-02-13: Cross-cut gate passed for the sprint batch: `pnpm unit-test`, `pnpm integration-test`, and `pnpm runtime:smoke`.
30 changes: 30 additions & 0 deletions apps/desktop-main/src/ipc/api-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { IpcMain } from 'electron';
import {
apiGetOperationDiagnosticsRequestSchema,
apiInvokeRequestSchema,
IPC_CHANNELS,
} from '@electron-foundation/contracts';
import type { MainIpcContext } from './handler-context';
import { registerValidatedHandler } from './register-validated-handler';

export const registerApiIpcHandlers = (
ipcMain: IpcMain,
context: MainIpcContext,
) => {
registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.apiInvoke,
schema: apiInvokeRequestSchema,
context,
handler: (_event, request) => context.invokeApiOperation(request),
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.apiGetOperationDiagnostics,
schema: apiGetOperationDiagnosticsRequestSchema,
context,
handler: (_event, request) =>
context.getApiOperationDiagnostics(request.payload.operationId),
});
};
46 changes: 46 additions & 0 deletions apps/desktop-main/src/ipc/app-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { IpcMain } from 'electron';
import {
appRuntimeVersionsRequestSchema,
appVersionRequestSchema,
asSuccess,
CONTRACT_VERSION,
handshakeRequestSchema,
IPC_CHANNELS,
} from '@electron-foundation/contracts';
import type { MainIpcContext } from './handler-context';
import { registerValidatedHandler } from './register-validated-handler';

export const registerAppIpcHandlers = (
ipcMain: IpcMain,
context: MainIpcContext,
) => {
registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.handshake,
schema: handshakeRequestSchema,
context,
handler: () => asSuccess({ contractVersion: CONTRACT_VERSION }),
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.appGetVersion,
schema: appVersionRequestSchema,
context,
handler: () => asSuccess({ version: context.appVersion }),
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.appGetRuntimeVersions,
schema: appRuntimeVersionsRequestSchema,
context,
handler: () =>
asSuccess({
electron: process.versions.electron,
node: process.versions.node,
chrome: process.versions.chrome,
appEnvironment: context.appEnvironment,
}),
});
};
100 changes: 100 additions & 0 deletions apps/desktop-main/src/ipc/auth-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { IpcMain } from 'electron';
import {
asFailure,
asSuccess,
authGetSessionSummaryRequestSchema,
authGetTokenDiagnosticsRequestSchema,
authSignInRequestSchema,
authSignOutRequestSchema,
IPC_CHANNELS,
} from '@electron-foundation/contracts';
import type { MainIpcContext } from './handler-context';
import { registerValidatedHandler } from './register-validated-handler';

export const registerAuthIpcHandlers = (
ipcMain: IpcMain,
context: MainIpcContext,
) => {
registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.authSignIn,
schema: authSignInRequestSchema,
context,
handler: (event, request) => {
const oidcService = context.getOidcService();
if (!oidcService) {
return asFailure(
'AUTH/NOT_CONFIGURED',
'OIDC authentication is not configured for this build.',
undefined,
false,
request.correlationId,
);
}

return oidcService.signIn();
},
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.authSignOut,
schema: authSignOutRequestSchema,
context,
handler: () => {
const oidcService = context.getOidcService();
if (!oidcService) {
return asSuccess({ signedOut: true });
}

return oidcService.signOut();
},
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.authGetSessionSummary,
schema: authGetSessionSummaryRequestSchema,
context,
handler: () => {
const oidcService = context.getOidcService();
if (!oidcService) {
return asSuccess({
state: 'signed-out' as const,
scopes: [],
entitlements: [],
});
}

return oidcService.getSessionSummary();
},
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.authGetTokenDiagnostics,
schema: authGetTokenDiagnosticsRequestSchema,
context,
handler: () => {
const oidcService = context.getOidcService();
if (!oidcService) {
return asSuccess({
sessionState: 'signed-out' as const,
bearerSource: 'access_token' as const,
accessToken: {
present: false,
format: 'absent' as const,
claims: null,
},
idToken: {
present: false,
format: 'absent' as const,
claims: null,
},
});
}

return oidcService.getTokenDiagnostics();
},
});
};
116 changes: 116 additions & 0 deletions apps/desktop-main/src/ipc/file-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { randomUUID } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { BrowserWindow, dialog, type IpcMain } from 'electron';
import {
asFailure,
asSuccess,
IPC_CHANNELS,
openFileDialogRequestSchema,
readTextFileRequestSchema,
} from '@electron-foundation/contracts';
import type { MainIpcContext } from './handler-context';
import { registerValidatedHandler } from './register-validated-handler';

export const registerFileIpcHandlers = (
ipcMain: IpcMain,
context: MainIpcContext,
) => {
registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.dialogOpenFile,
schema: openFileDialogRequestSchema,
context,
handler: async (event, request) => {
const result = await dialog.showOpenDialog({
title: request.payload.title,
filters: request.payload.filters,
properties: ['openFile'],
});

const selectedPath = result.filePaths[0];
if (result.canceled || !selectedPath) {
return asSuccess({ canceled: true });
}

const windowId = BrowserWindow.fromWebContents(event.sender)?.id;
if (!windowId) {
return asFailure(
'FS/TOKEN_CREATION_FAILED',
'Unable to associate selected file with a window context.',
undefined,
false,
request.correlationId,
);
}

const fileToken = randomUUID();
context.selectedFileTokens.set(fileToken, {
filePath: selectedPath,
windowId,
expiresAt: Date.now() + context.fileTokenTtlMs,
});

return asSuccess({
canceled: false,
fileName: path.basename(selectedPath),
fileToken,
});
},
});

registerValidatedHandler({
ipcMain,
channel: IPC_CHANNELS.fsReadTextFile,
schema: readTextFileRequestSchema,
context,
handler: async (event, request) => {
try {
const selected = context.selectedFileTokens.get(
request.payload.fileToken,
);
if (!selected || selected.expiresAt <= Date.now()) {
context.selectedFileTokens.delete(request.payload.fileToken);
return asFailure(
'FS/INVALID_TOKEN',
'The selected file token is invalid or expired.',
undefined,
false,
request.correlationId,
);
}

const senderWindowId = BrowserWindow.fromWebContents(event.sender)?.id;
if (senderWindowId !== selected.windowId) {
context.selectedFileTokens.delete(request.payload.fileToken);
return asFailure(
'FS/INVALID_TOKEN_SCOPE',
'Selected file token was issued for a different window.',
{
senderWindowId: senderWindowId ?? null,
tokenWindowId: selected.windowId,
},
false,
request.correlationId,
);
}

context.selectedFileTokens.delete(request.payload.fileToken);

const content = await fs.readFile(selected.filePath, {
encoding: request.payload.encoding,
});

return asSuccess({ content });
} catch (error) {
return asFailure(
'FS/READ_FAILED',
'Unable to read requested file.',
error,
false,
request.correlationId,
);
}
},
});
};
41 changes: 41 additions & 0 deletions apps/desktop-main/src/ipc/handler-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { IpcMainInvokeEvent } from 'electron';
import type {
ApiGetOperationDiagnosticsResponse,
ApiInvokeRequest,
ApiInvokeResponse,
DesktopResult,
} from '@electron-foundation/contracts';
import type { OidcService } from '../oidc-service';
import type { StorageGateway } from '../storage-gateway';

export type FileSelectionToken = {
filePath: string;
expiresAt: number;
windowId: number;
};

export type MainIpcContext = {
appVersion: string;
appEnvironment: 'development' | 'staging' | 'production';
fileTokenTtlMs: number;
selectedFileTokens: Map<string, FileSelectionToken>;
getCorrelationId: (payload: unknown) => string | undefined;
assertAuthorizedSender: (
event: IpcMainInvokeEvent,
correlationId?: string,
) => DesktopResult<never> | null;
getOidcService: () => OidcService | null;
getStorageGateway: () => StorageGateway;
invokeApiOperation: (
request: ApiInvokeRequest,
) => Promise<DesktopResult<ApiInvokeResponse>>;
getApiOperationDiagnostics: (
operationId: ApiInvokeRequest['payload']['operationId'],
) => DesktopResult<ApiGetOperationDiagnosticsResponse>;
logEvent: (
level: 'debug' | 'info' | 'warn' | 'error',
event: string,
correlationId?: string,
details?: Record<string, unknown>,
) => void;
};
22 changes: 22 additions & 0 deletions apps/desktop-main/src/ipc/register-ipc-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { IpcMain } from 'electron';
import type { MainIpcContext } from './handler-context';
import { registerApiIpcHandlers } from './api-handlers';
import { registerAppIpcHandlers } from './app-handlers';
import { registerAuthIpcHandlers } from './auth-handlers';
import { registerFileIpcHandlers } from './file-handlers';
import { registerStorageIpcHandlers } from './storage-handlers';
import { registerTelemetryIpcHandlers } from './telemetry-handlers';
import { registerUpdatesIpcHandlers } from './updates-handlers';

export const registerIpcHandlers = (
ipcMain: IpcMain,
context: MainIpcContext,
) => {
registerAppIpcHandlers(ipcMain, context);
registerAuthIpcHandlers(ipcMain, context);
registerFileIpcHandlers(ipcMain, context);
registerApiIpcHandlers(ipcMain, context);
registerStorageIpcHandlers(ipcMain, context);
registerUpdatesIpcHandlers(ipcMain, context);
registerTelemetryIpcHandlers(ipcMain, context);
};
Loading