diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 8e1ee1db4a..4922e02e82 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -13,6 +13,7 @@ import type { GenerateMcpSamplingResponseFunction } from '~/common/plugins/types import type { RenderedRequest } from '~/common/templating/types'; import { invariant } from '~/common/utils/invariant'; import { invokeWithNormalizedError } from '~/main/ipc/invoke'; +import { resolveServicesInvokeChannel } from '~/main/ipc/migrated-services-invoke-pairs'; import type { LLMBackend, LLMConfig, LLMConfigServiceAPI } from '~/main/llm-config-service'; import type { PluginInvokeMethod } from '~/plugins/invoke-method'; import { isUserAbortResolveMergeConflictError, UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; @@ -552,8 +553,12 @@ if (process.contextIsolated) { // function and rebuild the services Proxy in the isolated renderer world. contextBridge.exposeInMainWorld( '_dataServicesInvoke', - (serviceName: string, methodName: string, ...args: unknown[]) => - invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args), + (serviceName: string, methodName: string, ...args: unknown[]) => { + const channel = resolveServicesInvokeChannel(serviceName, methodName); + return channel === 'services.invoke' + ? invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args) + : invokeWithNormalizedError(channel, ...args); + }, ); contextBridge.exposeInMainWorld('env', env); } else { diff --git a/packages/insomnia/src/main/ipc/__tests__/__snapshots__/services-invoke-surface.test.ts.snap b/packages/insomnia/src/main/ipc/__tests__/__snapshots__/services-invoke-surface.test.ts.snap index 7897d2814f..bbe833c3c4 100644 --- a/packages/insomnia/src/main/ipc/__tests__/__snapshots__/services-invoke-surface.test.ts.snap +++ b/packages/insomnia/src/main/ipc/__tests__/__snapshots__/services-invoke-surface.test.ts.snap @@ -6,16 +6,16 @@ exports[`describeServicesInvokeSurface (real repo) > matches the current service "apiSpec.getOrCreateForParentId: services.invoke (1 call site)", "apiSpec.update: services.invoke (1 call site)", "apiSpec.updateOrCreateForParentId: services.invoke (1 call site)", - "caCertificate.create: services.invoke (1 call site)", - "caCertificate.getById: services.invoke (1 call site)", - "caCertificate.getByParentId: services.invoke (4 call sites)", - "caCertificate.removeWhere: services.invoke (1 call site)", - "caCertificate.update: services.invoke (1 call site)", - "clientCertificate.create: services.invoke (1 call site)", - "clientCertificate.findByParentId: services.invoke (5 call sites)", - "clientCertificate.getById: services.invoke (2 call sites)", - "clientCertificate.remove: services.invoke (1 call site)", - "clientCertificate.update: services.invoke (1 call site)", + "caCertificate.create: named handler (1 call site)", + "caCertificate.getById: named handler (1 call site)", + "caCertificate.getByParentId: named handler (4 call sites)", + "caCertificate.removeWhere: named handler (1 call site)", + "caCertificate.update: named handler (1 call site)", + "clientCertificate.create: named handler (1 call site)", + "clientCertificate.findByParentId: named handler (5 call sites)", + "clientCertificate.getById: named handler (2 call sites)", + "clientCertificate.remove: named handler (1 call site)", + "clientCertificate.update: named handler (1 call site)", "cloudCredential.all: services.invoke (3 call sites)", "cloudCredential.create: services.invoke (1 call site)", "cloudCredential.getById: services.invoke (4 call sites)", diff --git a/packages/insomnia/src/main/ipc/__tests__/services-invoke-migrated-handlers.test.ts b/packages/insomnia/src/main/ipc/__tests__/services-invoke-migrated-handlers.test.ts new file mode 100644 index 0000000000..4a3b54a384 --- /dev/null +++ b/packages/insomnia/src/main/ipc/__tests__/services-invoke-migrated-handlers.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from 'vitest'; + +import * as handlers from '../services-invoke-migrated-handlers'; + +vi.mock('insomnia-data', () => ({ + services: { + caCertificate: { create: vi.fn(), getById: vi.fn(), getByParentId: vi.fn(), removeWhere: vi.fn(), update: vi.fn() }, + clientCertificate: { create: vi.fn(), findByParentId: vi.fn(), getById: vi.fn(), remove: vi.fn(), update: vi.fn() }, + }, +})); + +// Reference behavior: exactly what the old services.invoke reflection dispatch did for a +// (serviceName, methodName) pair — `services[serviceName][methodName](...args)`. Every migrated +// named handler below is checked against this, not just against its own naming convention. +const legacyDispatch = async (serviceName: string, methodName: string, ...args: unknown[]) => { + const { services } = await import('insomnia-data'); + const service = (services as unknown as Record unknown>>)[serviceName]; + return service[methodName](...args); +}; + +const CASES: { handlerName: keyof typeof handlers; serviceName: string; methodName: string; args: unknown[] }[] = [ + { handlerName: 'caCertificateCreate', serviceName: 'caCertificate', methodName: 'create', args: [{ parentId: 'w1' }] }, + { handlerName: 'caCertificateGetById', serviceName: 'caCertificate', methodName: 'getById', args: ['cert1'] }, + { handlerName: 'caCertificateGetByParentId', serviceName: 'caCertificate', methodName: 'getByParentId', args: ['w1'] }, + { handlerName: 'caCertificateRemoveWhere', serviceName: 'caCertificate', methodName: 'removeWhere', args: ['w1'] }, + { handlerName: 'caCertificateUpdate', serviceName: 'caCertificate', methodName: 'update', args: [{ _id: 'cert1' }, { path: '/new.pem' }] }, + { handlerName: 'clientCertificateCreate', serviceName: 'clientCertificate', methodName: 'create', args: [{ parentId: 'w1' }] }, + { handlerName: 'clientCertificateFindByParentId', serviceName: 'clientCertificate', methodName: 'findByParentId', args: ['w1'] }, + { handlerName: 'clientCertificateGetById', serviceName: 'clientCertificate', methodName: 'getById', args: ['cert1'] }, + { handlerName: 'clientCertificateRemove', serviceName: 'clientCertificate', methodName: 'remove', args: [{ _id: 'cert1' }] }, + { handlerName: 'clientCertificateUpdate', serviceName: 'clientCertificate', methodName: 'update', args: [{ _id: 'cert1' }, { host: 'localhost' }] }, +]; + +describe.each(CASES)('$handlerName', ({ handlerName, serviceName, methodName, args }) => { + it('forwards to the same services.* call, with the same args and result, as the old services.invoke dispatch', async () => { + const { services } = await import('insomnia-data'); + const service = (services as unknown as Record>>)[serviceName]; + service[methodName].mockReset().mockResolvedValue({ sentinel: `${handlerName}-result` }); + + const legacyResult = await legacyDispatch(serviceName, methodName, ...args); + const namedHandlerResult = await (handlers[handlerName] as (...a: unknown[]) => unknown)({}, ...args); + + expect(namedHandlerResult).toEqual(legacyResult); + expect(service[methodName]).toHaveBeenCalledWith(...args); + }); +}); diff --git a/packages/insomnia/src/main/ipc/__tests__/services-invoke-surface.test.ts b/packages/insomnia/src/main/ipc/__tests__/services-invoke-surface.test.ts index 0468caabd4..65b6135b98 100644 --- a/packages/insomnia/src/main/ipc/__tests__/services-invoke-surface.test.ts +++ b/packages/insomnia/src/main/ipc/__tests__/services-invoke-surface.test.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { MIGRATED_SERVICES_INVOKE_PAIRS } from '../migrated-services-invoke-pairs'; import { describeServicesInvokeSurface, findPairsMissingNamedHandler, @@ -96,4 +97,14 @@ describe('describeServicesInvokeSurface (real repo)', () => { expect(entries.length).toBeGreaterThan(0); expect(formatServicesInvokeSurfaceEntries(entries)).toMatchSnapshot(); }); + + // Keeps the renderer-transport allowlist (migrated-services-invoke-pairs.ts) from drifting out of + // sync with what main/ipc/*.ts actually registers: a pair the proxy would route to a named channel + // that doesn't exist would hard-fail every call; a pair with a named handler the proxy doesn't know + // about would silently keep using the generic gateway. + it('has a named handler for every pair the renderer proxy is told to route there, and vice versa', () => { + const entries = describeServicesInvokeSurface(); + const detectedMigrated = new Set(entries.filter(e => e.hasNamedHandler).map(e => e.pair)); + expect(detectedMigrated).toEqual(new Set(MIGRATED_SERVICES_INVOKE_PAIRS)); + }); }); diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 40614437ef..348b494e26 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -43,6 +43,16 @@ export type HandleChannels = | 'curlRequest' | 'database.caCertificate.create' | 'services.invoke' + | 'services.caCertificate.create' + | 'services.caCertificate.getById' + | 'services.caCertificate.getByParentId' + | 'services.caCertificate.removeWhere' + | 'services.caCertificate.update' + | 'services.clientCertificate.create' + | 'services.clientCertificate.findByParentId' + | 'services.clientCertificate.getById' + | 'services.clientCertificate.remove' + | 'services.clientCertificate.update' | 'extractJsonFileFromPostmanDataDumpArchive' | 'generateCommitsFromDiff' | 'generateMockRouteDataFromSpec' diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index a009e0a864..1880679f49 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -44,6 +44,18 @@ import { exportWorkspacesHAR, } from '~/main/har'; import { convert } from '~/main/importers/convert'; +import { + caCertificateCreate, + caCertificateGetById, + caCertificateGetByParentId, + caCertificateRemoveWhere, + caCertificateUpdate, + clientCertificateCreate, + clientCertificateFindByParentId, + clientCertificateGetById, + clientCertificateRemove, + clientCertificateUpdate, +} from '~/main/ipc/services-invoke-migrated-handlers'; import { getCurrentConfig, type LLMConfigServiceAPI } from '~/main/llm-config-service'; import { multipartBufferToArray, type Part } from '~/main/multipart-buffer-to-array'; import { insecureReadFile, insecureReadFileWithEncoding, isPathAllowed, secureReadFile } from '~/main/secure-read-file'; @@ -396,6 +408,16 @@ export function registerMainHandlers() { ipcMainHandle('database.caCertificate.create', async (_, options: { parentId: string; path: string }) => { return services.caCertificate.create(options); }); + ipcMainHandle('services.caCertificate.create', caCertificateCreate); + ipcMainHandle('services.caCertificate.getById', caCertificateGetById); + ipcMainHandle('services.caCertificate.getByParentId', caCertificateGetByParentId); + ipcMainHandle('services.caCertificate.removeWhere', caCertificateRemoveWhere); + ipcMainHandle('services.caCertificate.update', caCertificateUpdate); + ipcMainHandle('services.clientCertificate.create', clientCertificateCreate); + ipcMainHandle('services.clientCertificate.findByParentId', clientCertificateFindByParentId); + ipcMainHandle('services.clientCertificate.getById', clientCertificateGetById); + ipcMainHandle('services.clientCertificate.remove', clientCertificateRemove); + ipcMainHandle('services.clientCertificate.update', clientCertificateUpdate); ipcMainHandle('createPlugin', async (_, options: { pluginName: string; mainJs: string }) => { return createPlugin(options.pluginName, options.mainJs); }); diff --git a/packages/insomnia/src/main/ipc/migrated-services-invoke-pairs.ts b/packages/insomnia/src/main/ipc/migrated-services-invoke-pairs.ts new file mode 100644 index 0000000000..a12987e13f --- /dev/null +++ b/packages/insomnia/src/main/ipc/migrated-services-invoke-pairs.ts @@ -0,0 +1,24 @@ +// Single source of truth for which (serviceName, methodName) pairs have their own +// `services..` IPC handler instead of routing through the generic +// `services.invoke` gateway (see services-invoke-surface.ts, SERVICES-INVOKE-MIGRATION-PLAN.md). +// Consulted by both the preload bridge and the non-isolated renderer proxy fallback so the two +// transports can never pick a different channel for the same pair. No Node-only imports here — this +// module is bundled into the renderer as well as the main process. +export const MIGRATED_SERVICES_INVOKE_PAIRS: ReadonlySet = new Set([ + 'caCertificate.create', + 'caCertificate.getById', + 'caCertificate.getByParentId', + 'caCertificate.removeWhere', + 'caCertificate.update', + 'clientCertificate.create', + 'clientCertificate.findByParentId', + 'clientCertificate.getById', + 'clientCertificate.remove', + 'clientCertificate.update', +]); + +/** The IPC channel a `services..` call should use: the named channel once migrated, else the legacy generic gateway. */ +export const resolveServicesInvokeChannel = (serviceName: string, methodName: string): string => + MIGRATED_SERVICES_INVOKE_PAIRS.has(`${serviceName}.${methodName}`) + ? `services.${serviceName}.${methodName}` + : 'services.invoke'; diff --git a/packages/insomnia/src/main/ipc/services-invoke-migrated-handlers.ts b/packages/insomnia/src/main/ipc/services-invoke-migrated-handlers.ts new file mode 100644 index 0000000000..ebdb02b415 --- /dev/null +++ b/packages/insomnia/src/main/ipc/services-invoke-migrated-handlers.ts @@ -0,0 +1,19 @@ +import type { CaCertificate, ClientCertificate } from 'insomnia-data'; +import { services } from 'insomnia-data'; + +// Named per-pair handlers for services.invoke pairs migrated off the generic reflection-based +// gateway (see services-invoke-surface.ts, SERVICES-INVOKE-MIGRATION-PLAN.md). Each forwards to the +// exact same services.* call the generic dispatch made for that pair, with the same arguments — main.ts +// registers each of these under the literal channel name `services..`. + +export const caCertificateCreate = (_: unknown, patch: Partial = {}) => services.caCertificate.create(patch); +export const caCertificateGetById = (_: unknown, id: string) => services.caCertificate.getById(id); +export const caCertificateGetByParentId = (_: unknown, parentId: string) => services.caCertificate.getByParentId(parentId); +export const caCertificateRemoveWhere = (_: unknown, parentId: string) => services.caCertificate.removeWhere(parentId); +export const caCertificateUpdate = (_: unknown, cert: CaCertificate, patch: Partial = {}) => services.caCertificate.update(cert, patch); + +export const clientCertificateCreate = (_: unknown, patch: Partial = {}) => services.clientCertificate.create(patch); +export const clientCertificateFindByParentId = (_: unknown, parentId: string) => services.clientCertificate.findByParentId(parentId); +export const clientCertificateGetById = (_: unknown, id: string) => services.clientCertificate.getById(id); +export const clientCertificateRemove = (_: unknown, cert: ClientCertificate) => services.clientCertificate.remove(cert); +export const clientCertificateUpdate = (_: unknown, cert: ClientCertificate, patch: Partial = {}) => services.clientCertificate.update(cert, patch); diff --git a/packages/insomnia/src/ui/renderer-services-proxy.ts b/packages/insomnia/src/ui/renderer-services-proxy.ts index de084c80d4..bc9d2014e7 100644 --- a/packages/insomnia/src/ui/renderer-services-proxy.ts +++ b/packages/insomnia/src/ui/renderer-services-proxy.ts @@ -1,6 +1,10 @@ import { invokeWithNormalizedError } from '~/main/ipc/invoke'; +import { resolveServicesInvokeChannel } from '~/main/ipc/migrated-services-invoke-pairs'; import { createServicesProxy } from '~/ui/services-proxy'; -export const servicesProxy = createServicesProxy((serviceName, methodName, ...args) => - invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args), -); +export const servicesProxy = createServicesProxy((serviceName, methodName, ...args) => { + const channel = resolveServicesInvokeChannel(serviceName, methodName); + return channel === 'services.invoke' + ? invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args) + : invokeWithNormalizedError(channel, ...args); +});