From 2f7496e35be56e6abd1df3788557210a9c71c7da Mon Sep 17 00:00:00 2001 From: xiaodemen Date: Thu, 13 Aug 2026 17:54:50 +0800 Subject: [PATCH] refactor(vcs): move vcs to insomnia-vcs refactor(vcs): move vcs to insomnia-vcs fix: fix comments --- package-lock.json | 21 +- package.json | 2 + .../__schemas__/model-schemas.ts | 37 +++ .../deterministic-stringify.test.ts | 2 +- .../common-src}/deterministic-stringify.ts | 0 packages/insomnia-data/common-src/index.ts | 1 + packages/insomnia-data/package.json | 1 + packages/insomnia-data/src/models/index.ts | 2 + .../src/models/utils}/ignore-keys.test.ts | 4 +- .../src/models/utils}/ignore-keys.ts | 0 packages/insomnia-vcs/package.json | 36 +++ packages/insomnia-vcs/setup-vitest.ts | 14 + .../src/__schemas__/model-schemas.ts | 37 +++ .../src}/__schemas__/type-schemas.ts | 2 +- .../src}/__tests__/crypt.test.ts | 0 .../src}/__tests__/util.test.ts | 6 +- .../src}/__tests__/vcs.test.ts | 58 +--- packages/insomnia-vcs/src/crypt.ts | 245 +++++++++++++++++ packages/insomnia-vcs/src/index.ts | 9 + .../src}/store/__tests__/index.test.ts | 0 .../src}/store/drivers/base.ts | 0 .../src}/store/drivers/file-system-driver.ts | 0 .../src}/store/drivers/graceful-rename.ts | 0 .../src}/store/drivers/memory-driver.ts | 0 .../store/hooks/__tests__/compress.test.ts | 0 .../src}/store/hooks/compress.ts | 0 .../core => insomnia-vcs/src}/store/index.ts | 0 .../src/sync => insomnia-vcs/src}/types.ts | 7 +- .../core => insomnia-vcs/src}/util.ts | 10 +- .../core => insomnia-vcs/src}/vcs.ts | 88 ++++-- packages/insomnia-vcs/tsconfig.json | 25 ++ packages/insomnia-vcs/tsconfig.test.json | 8 + packages/insomnia-vcs/vitest.config.ts | 5 + packages/insomnia/package.json | 3 - packages/insomnia/src/__mocks__/node-forge.ts | 99 ------- packages/insomnia/src/common/account/crypt.ts | 250 +----------------- packages/insomnia/src/entry.preload.ts | 2 +- .../src/main/cloud-sync/create-vcs.ts | 12 - packages/insomnia/src/main/cloud-sync/ipc.ts | 2 +- .../main/cloud-sync/pull-backend-project.ts | 3 +- packages/insomnia/src/main/cloud-sync/vcs.ts | 28 +- packages/insomnia/src/main/git-service.ts | 13 +- ...workspaceId.insomnia-sync.branch.merge.tsx | 2 +- packages/insomnia/src/sync/git/git-vcs.ts | 3 +- .../insomnia/src/sync/git/ne-db-client.ts | 3 +- .../sync/vcs/initialize-backend-project.ts | 2 +- .../src/sync/vcs/{errors.ts => utils.ts} | 11 + .../dropdowns/git-project-sync-dropdown.tsx | 2 +- .../dropdowns/git-sync-dropdown.tsx | 2 +- .../components/modals/git-branches-modal.tsx | 2 +- .../modals/git-project-branches-modal.tsx | 2 +- .../components/modals/sync-history-modal.tsx | 2 +- .../ui/components/modals/sync-merge-modal.tsx | 3 +- .../components/modals/sync-staging-modal.tsx | 2 +- packages/insomnia/src/ui/sync-utils.ts | 2 +- .../ui/utils/__tests__/insomnia-sync.test.ts | 2 +- .../insomnia/src/ui/utils/insomnia-sync.ts | 4 +- .../insomnia/src/ui/utils/remote-projects.ts | 2 +- packages/insomnia/types/node-forge-lib.d.ts | 10 - 59 files changed, 592 insertions(+), 496 deletions(-) create mode 100644 packages/insomnia-data/__schemas__/model-schemas.ts rename packages/{insomnia/src/sync/lib/__tests__ => insomnia-data/common-src}/deterministic-stringify.test.ts (93%) rename packages/{insomnia/src/sync/lib => insomnia-data/common-src}/deterministic-stringify.ts (100%) rename packages/{insomnia/src/sync/__tests__ => insomnia-data/src/models/utils}/ignore-keys.test.ts (92%) rename packages/{insomnia/src/sync => insomnia-data/src/models/utils}/ignore-keys.ts (100%) create mode 100644 packages/insomnia-vcs/package.json create mode 100644 packages/insomnia-vcs/setup-vitest.ts create mode 100644 packages/insomnia-vcs/src/__schemas__/model-schemas.ts rename packages/{insomnia/src/sync => insomnia-vcs/src}/__schemas__/type-schemas.ts (95%) rename packages/{insomnia/src/common/account => insomnia-vcs/src}/__tests__/crypt.test.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/__tests__/util.test.ts (99%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/__tests__/vcs.test.ts (93%) create mode 100644 packages/insomnia-vcs/src/crypt.ts create mode 100644 packages/insomnia-vcs/src/index.ts rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/__tests__/index.test.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/drivers/base.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/drivers/file-system-driver.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/drivers/graceful-rename.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/drivers/memory-driver.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/hooks/__tests__/compress.test.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/hooks/compress.ts (100%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/store/index.ts (100%) rename packages/{insomnia/src/sync => insomnia-vcs/src}/types.ts (94%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/util.ts (98%) rename packages/{insomnia/src/main/cloud-sync/core => insomnia-vcs/src}/vcs.ts (96%) create mode 100644 packages/insomnia-vcs/tsconfig.json create mode 100644 packages/insomnia-vcs/tsconfig.test.json create mode 100644 packages/insomnia-vcs/vitest.config.ts delete mode 100644 packages/insomnia/src/__mocks__/node-forge.ts delete mode 100644 packages/insomnia/src/main/cloud-sync/create-vcs.ts rename packages/insomnia/src/sync/vcs/{errors.ts => utils.ts} (63%) delete mode 100644 packages/insomnia/types/node-forge-lib.d.ts diff --git a/package-lock.json b/package-lock.json index b7049295ca7f..afecb22c28fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "packages/insomnia-testing", "packages/insomnia", "packages/insomnia-data", + "packages/insomnia-vcs", "packages/insomnia-analytics", "packages/insomnia-api", "packages/insomnia-inso", @@ -24,6 +25,7 @@ "ajv": "^8.17.1" }, "devDependencies": { + "@develohpanda/fluent-builder": "^2.1.2", "@eslint/js": "^9.23.0", "@types/chai": "^4.3.14", "@types/har-format": "^1.2.15", @@ -16779,6 +16781,10 @@ "resolved": "packages/insomnia-testing", "link": true }, + "node_modules/insomnia-vcs": { + "resolved": "packages/insomnia-vcs", + "link": true + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -27519,7 +27525,6 @@ "mocha": "^11.7.5", "monaco-editor": "^0.52.2", "multiparty": "^4.2.3", - "node-forge": "^1.3.1", "oauth-1.0a": "^2.2.6", "objectpath": "^2.0.0", "papaparse": "^5.5.2", @@ -27551,7 +27556,6 @@ "insomnia": "bin/yarn-standalone.js" }, "devDependencies": { - "@develohpanda/fluent-builder": "^2.1.2", "@modelcontextprotocol/sdk": "^1.17.5", "@react-router/dev": "7.18.2", "@tailwindcss/vite": "^4.1.17", @@ -27577,7 +27581,6 @@ "@types/multiparty": "^0.0.36", "@types/ncp": "^2.0.8", "@types/nedb": "^1.8.16", - "@types/node-forge": "^1.3.11", "@types/papaparse": "^5.3.15", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", @@ -27630,6 +27633,7 @@ "license": "Apache-2.0", "dependencies": { "@seald-io/nedb": "^4.1.1", + "clone": "^2.1.2", "deep-equal": "2.2.3", "fuzzysort": "^1.9.0", "graphql": "^16.10.0", @@ -28045,6 +28049,17 @@ "chai-json-schema": "1.5.1" } }, + "packages/insomnia-vcs": { + "version": "13.1.0", + "license": "Apache-2.0", + "dependencies": { + "clone": "^2.1.2", + "node-forge": "^1.4.0" + }, + "devDependencies": { + "@types/node-forge": "^1.3.14" + } + }, "packages/insomnia/node_modules/@rjsf/core": { "version": "6.0.0-beta.15", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.0.0-beta.15.tgz", diff --git a/package.json b/package.json index 10c9afff1229..1c12696ae00c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "packages/insomnia-testing", "packages/insomnia", "packages/insomnia-data", + "packages/insomnia-vcs", "packages/insomnia-analytics", "packages/insomnia-api", "packages/insomnia-inso", @@ -47,6 +48,7 @@ "check-cycle-references": "madge --circular --extensions ts,tsx packages" }, "devDependencies": { + "@develohpanda/fluent-builder": "^2.1.2", "@eslint/js": "^9.23.0", "@types/chai": "^4.3.14", "@types/har-format": "^1.2.15", diff --git a/packages/insomnia-data/__schemas__/model-schemas.ts b/packages/insomnia-data/__schemas__/model-schemas.ts new file mode 100644 index 000000000000..b656d9f8d9bb --- /dev/null +++ b/packages/insomnia-data/__schemas__/model-schemas.ts @@ -0,0 +1,37 @@ +import type { Schema } from '@develohpanda/fluent-builder'; +import clone from 'clone'; +import type { AllTypes, BaseModel, Workspace } from 'insomnia-data'; +import { models } from 'insomnia-data'; + +const { workspace } = models; + +// move into fluent-builder +const toSchema = (obj: T): Schema => { + const cloned = clone(obj); + const output: Partial> = {}; + + // @ts-expect-error -- mapping unsoundness + Object.keys(cloned).forEach(key => { + // @ts-expect-error -- mapping unsoundness + output[key] = () => cloned[key]; + }); + + return output as Schema; +}; + +export const baseModelSchema: Schema = { + _id: () => 'id', + created: () => 1234, + isPrivate: () => false, + modified: () => 5678, + name: () => 'name', + parentId: () => '', + type: () => 'base' as AllTypes, +}; + +export const workspaceModelSchema: Schema = { + ...baseModelSchema, + ...toSchema(workspace.init()), + certificates: () => {}, + type: () => workspace.type, +}; diff --git a/packages/insomnia/src/sync/lib/__tests__/deterministic-stringify.test.ts b/packages/insomnia-data/common-src/deterministic-stringify.test.ts similarity index 93% rename from packages/insomnia/src/sync/lib/__tests__/deterministic-stringify.test.ts rename to packages/insomnia-data/common-src/deterministic-stringify.test.ts index 4ad961e98d04..2e9cab8b911f 100644 --- a/packages/insomnia/src/sync/lib/__tests__/deterministic-stringify.test.ts +++ b/packages/insomnia-data/common-src/deterministic-stringify.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { deterministicStringify } from '../deterministic-stringify'; +import { deterministicStringify } from './deterministic-stringify'; describe('deterministicStringify()', () => { it('sorts object keys', () => { diff --git a/packages/insomnia/src/sync/lib/deterministic-stringify.ts b/packages/insomnia-data/common-src/deterministic-stringify.ts similarity index 100% rename from packages/insomnia/src/sync/lib/deterministic-stringify.ts rename to packages/insomnia-data/common-src/deterministic-stringify.ts diff --git a/packages/insomnia-data/common-src/index.ts b/packages/insomnia-data/common-src/index.ts index f00c664b7df1..428fab57f2d2 100644 --- a/packages/insomnia-data/common-src/index.ts +++ b/packages/insomnia-data/common-src/index.ts @@ -11,3 +11,4 @@ export * from './hotkeys'; export * from './settings'; export * from './keyboard-keys'; export * from './search'; +export * from './deterministic-stringify'; diff --git a/packages/insomnia-data/package.json b/packages/insomnia-data/package.json index 426cfe1f20ac..813cc985a483 100644 --- a/packages/insomnia-data/package.json +++ b/packages/insomnia-data/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@seald-io/nedb": "^4.1.1", + "clone": "^2.1.2", "deep-equal": "2.2.3", "fuzzysort": "^1.9.0", "graphql": "^16.10.0", diff --git a/packages/insomnia-data/src/models/index.ts b/packages/insomnia-data/src/models/index.ts index f454b30fe7a1..0d7f9051e030 100644 --- a/packages/insomnia-data/src/models/index.ts +++ b/packages/insomnia-data/src/models/index.ts @@ -6,6 +6,8 @@ export * from './db-models'; export * as organization from './organization'; +export * from './utils/ignore-keys'; + // Type assertion to ensure dbModels has the expected structure dbModels satisfies Record< string, diff --git a/packages/insomnia/src/sync/__tests__/ignore-keys.test.ts b/packages/insomnia-data/src/models/utils/ignore-keys.test.ts similarity index 92% rename from packages/insomnia/src/sync/__tests__/ignore-keys.test.ts rename to packages/insomnia-data/src/models/utils/ignore-keys.test.ts index 1ba9be673204..3468de8589ea 100644 --- a/packages/insomnia/src/sync/__tests__/ignore-keys.test.ts +++ b/packages/insomnia-data/src/models/utils/ignore-keys.test.ts @@ -1,8 +1,8 @@ import { createBuilder } from '@develohpanda/fluent-builder'; import { describe, expect, it } from 'vitest'; -import { baseModelSchema, workspaceModelSchema } from '../__schemas__/model-schemas'; -import { deleteKeys, resetKeys, shouldIgnoreKey } from '../ignore-keys'; +import { baseModelSchema, workspaceModelSchema } from '../../../__schemas__/model-schemas'; +import { deleteKeys, resetKeys, shouldIgnoreKey } from './ignore-keys'; const baseModelBuilder = createBuilder(baseModelSchema); const workspaceModelBuilder = createBuilder(workspaceModelSchema); diff --git a/packages/insomnia/src/sync/ignore-keys.ts b/packages/insomnia-data/src/models/utils/ignore-keys.ts similarity index 100% rename from packages/insomnia/src/sync/ignore-keys.ts rename to packages/insomnia-data/src/models/utils/ignore-keys.ts diff --git a/packages/insomnia-vcs/package.json b/packages/insomnia-vcs/package.json new file mode 100644 index 000000000000..aaf15b43f693 --- /dev/null +++ b/packages/insomnia-vcs/package.json @@ -0,0 +1,36 @@ +{ + "name": "insomnia-vcs", + "version": "13.1.0", + "private": true, + "description": "Insomnia VCS functionalities", + "license": "Apache-2.0", + "author": "Kong ", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/Kong/insomnia.git", + "directory": "packages/insomnia-vcs" + }, + "bugs": { + "url": "https://github.com/kong/insomnia/issues" + }, + "homepage": "https://github.com/Kong/insomnia#readme", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "scripts": { + "lint": "eslint . --ext .ts --cache", + "type-check": "tsc --noEmit && tsc -p tsconfig.test.json", + "test": "vitest run" + }, + "dependencies": { + "clone": "^2.1.2", + "node-forge": "^1.4.0" + }, + "devDependencies": { + "@types/node-forge": "^1.3.14" + } +} diff --git a/packages/insomnia-vcs/setup-vitest.ts b/packages/insomnia-vcs/setup-vitest.ts new file mode 100644 index 000000000000..8fa200981fb9 --- /dev/null +++ b/packages/insomnia-vcs/setup-vitest.ts @@ -0,0 +1,14 @@ +import { initDatabase, initServices } from 'insomnia-data'; +import { createNedbDatabase, servicesNodeImpl } from 'insomnia-data/node'; +import { vi } from 'vitest'; + +const database = createNedbDatabase(); +await initDatabase(database, { inMemoryOnly: true }, true); +await initServices(servicesNodeImpl); + +// eslint-disable-next-line no-restricted-imports +import { v4Mock } from '../insomnia-data/__mocks__/uuid'; + +vi.mock('uuid', () => ({ + v4: () => v4Mock(), +})); diff --git a/packages/insomnia-vcs/src/__schemas__/model-schemas.ts b/packages/insomnia-vcs/src/__schemas__/model-schemas.ts new file mode 100644 index 000000000000..b656d9f8d9bb --- /dev/null +++ b/packages/insomnia-vcs/src/__schemas__/model-schemas.ts @@ -0,0 +1,37 @@ +import type { Schema } from '@develohpanda/fluent-builder'; +import clone from 'clone'; +import type { AllTypes, BaseModel, Workspace } from 'insomnia-data'; +import { models } from 'insomnia-data'; + +const { workspace } = models; + +// move into fluent-builder +const toSchema = (obj: T): Schema => { + const cloned = clone(obj); + const output: Partial> = {}; + + // @ts-expect-error -- mapping unsoundness + Object.keys(cloned).forEach(key => { + // @ts-expect-error -- mapping unsoundness + output[key] = () => cloned[key]; + }); + + return output as Schema; +}; + +export const baseModelSchema: Schema = { + _id: () => 'id', + created: () => 1234, + isPrivate: () => false, + modified: () => 5678, + name: () => 'name', + parentId: () => '', + type: () => 'base' as AllTypes, +}; + +export const workspaceModelSchema: Schema = { + ...baseModelSchema, + ...toSchema(workspace.init()), + certificates: () => {}, + type: () => workspace.type, +}; diff --git a/packages/insomnia/src/sync/__schemas__/type-schemas.ts b/packages/insomnia-vcs/src/__schemas__/type-schemas.ts similarity index 95% rename from packages/insomnia/src/sync/__schemas__/type-schemas.ts rename to packages/insomnia-vcs/src/__schemas__/type-schemas.ts index 257cbb125191..80598912a5e2 100644 --- a/packages/insomnia/src/sync/__schemas__/type-schemas.ts +++ b/packages/insomnia-vcs/src/__schemas__/type-schemas.ts @@ -1,6 +1,5 @@ import { createBuilder, type Schema } from '@develohpanda/fluent-builder'; -import { baseModelSchema } from '../../sync/__schemas__/model-schemas'; import type { BackendProject, BackendProjectWithTeam, @@ -10,6 +9,7 @@ import type { StatusCandidate, Team, } from '../types'; +import { baseModelSchema } from './model-schemas'; export const projectSchema: Schema = { id: () => 'id', diff --git a/packages/insomnia/src/common/account/__tests__/crypt.test.ts b/packages/insomnia-vcs/src/__tests__/crypt.test.ts similarity index 100% rename from packages/insomnia/src/common/account/__tests__/crypt.test.ts rename to packages/insomnia-vcs/src/__tests__/crypt.test.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/__tests__/util.test.ts b/packages/insomnia-vcs/src/__tests__/util.test.ts similarity index 99% rename from packages/insomnia/src/main/cloud-sync/core/__tests__/util.test.ts rename to packages/insomnia-vcs/src/__tests__/util.test.ts index c50ba4fb64bf..8582164f564f 100644 --- a/packages/insomnia/src/main/cloud-sync/core/__tests__/util.test.ts +++ b/packages/insomnia-vcs/src/__tests__/util.test.ts @@ -1,11 +1,11 @@ // @ts-nocheck import { createBuilder } from '@develohpanda/fluent-builder'; +import type { StageEntry } from 'insomnia-data'; import { models } from 'insomnia-data'; import { beforeEach, describe, expect, it } from 'vitest'; -import { baseModelSchema, workspaceModelSchema } from '../../../../sync/__schemas__/model-schemas'; -import { branchSchema, mergeConflictSchema, statusCandidateSchema } from '../../../../sync/__schemas__/type-schemas'; -import type { StageEntry } from '../../../../sync/types'; +import { baseModelSchema, workspaceModelSchema } from '../__schemas__/model-schemas'; +import { branchSchema, mergeConflictSchema, statusCandidateSchema } from '../__schemas__/type-schemas'; import { combinedMapKeys, compareBranches, diff --git a/packages/insomnia/src/main/cloud-sync/core/__tests__/vcs.test.ts b/packages/insomnia-vcs/src/__tests__/vcs.test.ts similarity index 93% rename from packages/insomnia/src/main/cloud-sync/core/__tests__/vcs.test.ts rename to packages/insomnia-vcs/src/__tests__/vcs.test.ts index 4d428f2bbc54..7aea357ce93f 100644 --- a/packages/insomnia/src/main/cloud-sync/core/__tests__/vcs.test.ts +++ b/packages/insomnia-vcs/src/__tests__/vcs.test.ts @@ -1,45 +1,15 @@ // @ts-nocheck import { createBuilder } from '@develohpanda/fluent-builder'; +import type { BackendProject } from 'insomnia-data'; +import { models } from 'insomnia-data'; +import { deterministicStringify } from 'insomnia-data/common'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { baseModelSchema, workspaceModelSchema } from '../../../../sync/__schemas__/model-schemas'; -import { projectSchema } from '../../../../sync/__schemas__/type-schemas'; -import { shouldIgnoreKey } from '../../../../sync/ignore-keys'; -import { deterministicStringify } from '../../../../sync/lib/deterministic-stringify'; -import type { BackendProject } from '../../../../sync/types'; +import { baseModelSchema, workspaceModelSchema } from '../__schemas__/model-schemas'; +import { projectSchema } from '../__schemas__/type-schemas'; import MemoryDriver from '../store/drivers/memory-driver'; import { chunkArray, VCS } from '../vcs'; -// Every snapshot id asserted in this file is a sha1 over the backend project id, which comes from -// generateId('prj'). generateId ultimately calls uuid.v4(), which the shared setup mock -// (setup-vitest.ts) replaces with a fixed pool. That setup mock is declared *after* a top-level -// `await` that loads the cross-package insomnia-data graph, so in CI it is applied -// non-deterministically — when it misses, generateId returns a real random uuid and every asserted -// hash changes, which is what made this file flake (the same commit passing on one Test run and -// failing on another). -// -// Pin it deterministically here instead: mock generateId at this in-package module boundary (a -// test-file-local vi.mock of insomnia's own module, which is applied reliably) with a private -// counter over the same id array. generateId('prj') is the only uuid consumer these tests exercise, -// so drawing the array in order reproduces the exact sequence the assertions were recorded against, -// independent of whether the ambient uuid mock happened to apply. -vi.mock('~/common/misc', async importActual => { - const actual = await importActual>(); - const { v4UUIDs } = await import('../../../../../../insomnia-data/__mocks__/uuid'); - let i = 0; - return { - ...actual, - generateId: (prefix?: string) => { - const uuid = v4UUIDs[i++]; - if (!uuid) { - throw new Error('Not enough mocked v4 UUIDs to go around in vcs.test.ts'); - } - const id = uuid.replace(/-/g, ''); - return prefix ? `${prefix}_${id}` : id; - }, - }; -}); - const baseModelBuilder = createBuilder(baseModelSchema); const workspaceModelBuilder = createBuilder(workspaceModelSchema); const projectBuilder = createBuilder(projectSchema); @@ -49,7 +19,7 @@ function newDoc(id) { } async function vcs(branch) { - const v = new VCS(new MemoryDriver()); + const v = new VCS({ driver: new MemoryDriver() }); await v.switchAndCreateBackendProjectIfNotExist('workspace_1', 'Test Workspace'); await v.checkout([], branch); return v; @@ -67,7 +37,7 @@ function describeChanges(a, b): string[] { const allKeys = Object.keys({ ...a, ...b }); for (const key of allKeys) { - if (shouldIgnoreKey(key, a)) { + if (models.shouldIgnoreKey(key, a)) { continue; } @@ -1014,7 +984,7 @@ describe('VCS', () => { backendProject = projectBuilder.reset().build(); const driver = new MemoryDriver(); - vcs = new VCS(driver); + vcs = new VCS({ driver }); driver.setItem('/projects/', Buffer.from(JSON.stringify([backendProject]))); driver.setItem(`/projects/${backendProject.id}/`, Buffer.from('')); @@ -1041,7 +1011,7 @@ describe('VCS', () => { beforeEach(() => { driver = new MemoryDriver(); - vcs = new VCS(driver); + vcs = new VCS({ driver }); backendProject = projectBuilder.reset().build(); }); @@ -1051,10 +1021,7 @@ describe('VCS', () => { await vcs._storeBackendProject(backendProject); expect(setItemSpy).toHaveBeenCalledTimes(1); - expect(setItemSpy).toHaveBeenCalledWith( - `/projects/${backendProject.id}/meta.json`, - expect.any(Buffer), - ); + expect(setItemSpy).toHaveBeenCalledWith(`/projects/${backendProject.id}/meta.json`, expect.any(Buffer)); expect(await vcs._getBackendProjectById(backendProject.id)).toEqual(backendProject); }); @@ -1092,10 +1059,7 @@ describe('VCS', () => { await vcs._storeBackendProject(backendProject); expect(setItemSpy).toHaveBeenCalledTimes(1); - expect(setItemSpy).toHaveBeenCalledWith( - `/projects/${backendProject.id}/meta.json`, - expect.any(Buffer), - ); + expect(setItemSpy).toHaveBeenCalledWith(`/projects/${backendProject.id}/meta.json`, expect.any(Buffer)); }); }); diff --git a/packages/insomnia-vcs/src/crypt.ts b/packages/insomnia-vcs/src/crypt.ts new file mode 100644 index 000000000000..4a85806dd572 --- /dev/null +++ b/packages/insomnia-vcs/src/crypt.ts @@ -0,0 +1,245 @@ +import type { AESMessage } from 'insomnia-data'; +import forge from 'node-forge'; + +export type { AESMessage }; + +/** + * Encrypt with RSA256 public key + * + * @param publicKeyJWK + * @param plaintext + * @return String + */ +export function encryptRSAWithJWK(publicKeyJWK: JsonWebKey, plaintext: string) { + if (publicKeyJWK.alg !== 'RSA-OAEP-256') { + throw new Error('Public key algorithm was not RSA-OAEP-256'); + } else if (publicKeyJWK.kty !== 'RSA') { + throw new Error('Public key type was not RSA'); + } else if (!publicKeyJWK.key_ops || !publicKeyJWK.key_ops.find(o => o === 'encrypt')) { + throw new Error('Public key does not have "encrypt" op'); + } else if (!publicKeyJWK.n || !publicKeyJWK.e) { + throw new Error('Public key is missing parameters'); + } + + const encodedPlaintext = encodeURIComponent(plaintext); + + const n = _b64UrlToBigInt(publicKeyJWK.n); + + const e = _b64UrlToBigInt(publicKeyJWK.e); + + // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason + const publicKey = forge.rsa.setPublicKey(n, e); + const encrypted = publicKey.encrypt(encodedPlaintext, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + return forge.util.bytesToHex(encrypted); +} + +export function decryptRSAWithJWK(privateJWK: JsonWebKey, encryptedBlob: string) { + if ( + !privateJWK.n || + !privateJWK.e || + !privateJWK.d || + !privateJWK.p || + !privateJWK.q || + !privateJWK.dp || + !privateJWK.dq || + !privateJWK.qi + ) { + throw new Error('Private key is missing parameters'); + } + + const n = _b64UrlToBigInt(privateJWK.n); + const e = _b64UrlToBigInt(privateJWK.e); + const d = _b64UrlToBigInt(privateJWK.d); + const p = _b64UrlToBigInt(privateJWK.p); + const q = _b64UrlToBigInt(privateJWK.q); + const dP = _b64UrlToBigInt(privateJWK.dp); + const dQ = _b64UrlToBigInt(privateJWK.dq); + const qInv = _b64UrlToBigInt(privateJWK.qi); + + // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason + const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv); + const bytes = forge.util.hexToBytes(encryptedBlob); + const decrypted = privateKey.decrypt(bytes, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + return decodeURIComponent(decrypted); +} + +/** + * Encrypt data using symmetric key + * + * @param jwkOrKey JWK or string representing symmetric key + * @param buff data to encrypt + * @param additionalData any additional public data to attach + * @returns {{iv, t, d, ad}} + */ +export function encryptAESBuffer(jwkOrKey: string | JsonWebKey, buff: Buffer, additionalData = ''): AESMessage { + // TODO: Add assertion checks for JWK + const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); + const key = forge.util.hexToBytes(rawKey); + const iv = forge.random.getBytesSync(12); + const cipher = forge.cipher.createCipher('AES-GCM', key); + cipher.start({ + additionalData, + iv, + tagLength: 128, + }); + cipher.update(forge.util.createBuffer(buff)); + cipher.finish(); + return { + iv: forge.util.bytesToHex(iv), + // @ts-expect-error -- TSCONVERSION needs to be converted to string + t: forge.util.bytesToHex(cipher.mode.tag), + ad: forge.util.bytesToHex(additionalData), + // @ts-expect-error -- TSCONVERSION needs to be converted to string + d: forge.util.bytesToHex(cipher.output), + }; +} + +/** + * Encrypt data using symmetric key + * + * @param jwkOrKey JWK or string representing symmetric key + * @param plaintext string of data to encrypt + * @param additionalData any additional public data to attach + * @returns {{iv, t, d, ad}} + */ +export function encryptAES(jwkOrKey: string | JsonWebKey, plaintext: string, additionalData = ''): AESMessage { + // TODO: Add assertion checks for JWK + const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); + const key = forge.util.hexToBytes(rawKey); + const iv = forge.random.getBytesSync(12); + const cipher = forge.cipher.createCipher('AES-GCM', key); + // Plaintext could contain weird unicode, so we have to encode that + const encodedPlaintext = encodeURIComponent(plaintext); + cipher.start({ + additionalData, + iv, + tagLength: 128, + }); + cipher.update(forge.util.createBuffer(encodedPlaintext)); + cipher.finish(); + return { + iv: forge.util.bytesToHex(iv), + // @ts-expect-error -- TSCONVERSION needs to be converted to string + t: forge.util.bytesToHex(cipher.mode.tag), + ad: forge.util.bytesToHex(additionalData), + // @ts-expect-error -- TSCONVERSION needs to be converted to string + d: forge.util.bytesToHex(cipher.output), + }; +} + +/** + * Decrypt AES using a key + * + * @param jwkOrKey JWK or string representing symmetric key + * @param encryptedResult encryption data + * @returns String + */ +export function decryptAES(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) { + // TODO: Add assertion checks for JWK + const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); + const key = forge.util.hexToBytes(rawKey); + // ~~~~~~~~~~~~~~~~~~~~ // + // Decrypt with AES-GCM // + // ~~~~~~~~~~~~~~~~~~~~ // + const decipher = forge.cipher.createDecipher('AES-GCM', key); + decipher.start({ + iv: forge.util.hexToBytes(encryptedResult.iv), + tagLength: encryptedResult.t.length * 4, + // @ts-expect-error -- TSCONVERSION needs to be converted to string + tag: forge.util.hexToBytes(encryptedResult.t), + additionalData: forge.util.hexToBytes(encryptedResult.ad), + }); + decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); + + if (decipher.finish()) { + return decodeURIComponent(decipher.output.toString()); + } + throw new Error('Failed to decrypt data'); +} + +/** + * Decrypts AES using a key to buffer + * @param jwkOrKey + * @param encryptedResult + * @returns {string} + */ +export function decryptAESToBuffer(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) { + // TODO: Add assertion checks for JWK + const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); + const key = forge.util.hexToBytes(rawKey); + // ~~~~~~~~~~~~~~~~~~~~ // + // Decrypt with AES-GCM // + // ~~~~~~~~~~~~~~~~~~~~ // + const decipher = forge.cipher.createDecipher('AES-GCM', key); + decipher.start({ + iv: forge.util.hexToBytes(encryptedResult.iv), + tagLength: encryptedResult.t.length * 4, + // @ts-expect-error -- TSCONVERSION needs to be converted to string + tag: forge.util.hexToBytes(encryptedResult.t), + additionalData: forge.util.hexToBytes(encryptedResult.ad), + }); + decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); + + if (decipher.finish()) { + // @ts-expect-error -- TSCONVERSION needs to be converted to string + return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex'); + } + throw new Error('Failed to decrypt data'); +} + +/** + * Generate a random AES256 key for use with symmetric encryption + */ +export async function generateAES256Key() { + // globalThis.crypto resolves to WebCrypto in the renderer and Node global Web Crypto + // in the main process, keeping this module isomorphic. + const c = globalThis.crypto; + // @ts-expect-error -- TSCONVERSION: likely needs a module augmentation for webkit + const subtle = c ? c.subtle || c.webkitSubtle : null; + + if (subtle) { + console.log('[crypt] Using Native AES Key Generation'); + const key = await subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ); + return subtle.exportKey('jwk', key); + } + console.log('[crypt] Using Fallback Forge AES Key Generation'); + const key = forge.util.bytesToHex(forge.random.getBytesSync(32)); + return { + kty: 'oct', + alg: 'A256GCM', + ext: true, + key_ops: ['encrypt', 'decrypt'], + k: _hexToB64Url(key), + }; +} + +// ~~~~~~~~~~~~~~~~ // +// Helper Functions // +// ~~~~~~~~~~~~~~~~ // + +function _hexToB64Url(h: string) { + const bytes = forge.util.hexToBytes(h); + return btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +} + +function _b64UrlToBigInt(s: string) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- see below + // @ts-ignore -- unfortunately, we must ignore here instead of the usual expect-error because this mondule is being used by two different builds (`insomnia` and `insomnia-send-request`) and in one of them this line is an error (`insomnia-send-request`) and the other it is not ()`insomnia`). + return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16); +} + +function _b64UrlToHex(s: string) { + const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); + return forge.util.bytesToHex(atob(b64)); +} diff --git a/packages/insomnia-vcs/src/index.ts b/packages/insomnia-vcs/src/index.ts new file mode 100644 index 000000000000..879788369e0c --- /dev/null +++ b/packages/insomnia-vcs/src/index.ts @@ -0,0 +1,9 @@ +export { default as FileSystemDriver } from './store/drivers/file-system-driver'; + +export { default as MemoryDriver } from './store/drivers/memory-driver'; + +export { VCS, type VcsOptions } from './vcs'; + +export * from './crypt'; + +export * from './types'; diff --git a/packages/insomnia/src/main/cloud-sync/core/store/__tests__/index.test.ts b/packages/insomnia-vcs/src/store/__tests__/index.test.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/__tests__/index.test.ts rename to packages/insomnia-vcs/src/store/__tests__/index.test.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/drivers/base.ts b/packages/insomnia-vcs/src/store/drivers/base.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/drivers/base.ts rename to packages/insomnia-vcs/src/store/drivers/base.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/drivers/file-system-driver.ts b/packages/insomnia-vcs/src/store/drivers/file-system-driver.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/drivers/file-system-driver.ts rename to packages/insomnia-vcs/src/store/drivers/file-system-driver.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/drivers/graceful-rename.ts b/packages/insomnia-vcs/src/store/drivers/graceful-rename.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/drivers/graceful-rename.ts rename to packages/insomnia-vcs/src/store/drivers/graceful-rename.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/drivers/memory-driver.ts b/packages/insomnia-vcs/src/store/drivers/memory-driver.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/drivers/memory-driver.ts rename to packages/insomnia-vcs/src/store/drivers/memory-driver.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/hooks/__tests__/compress.test.ts b/packages/insomnia-vcs/src/store/hooks/__tests__/compress.test.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/hooks/__tests__/compress.test.ts rename to packages/insomnia-vcs/src/store/hooks/__tests__/compress.test.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/hooks/compress.ts b/packages/insomnia-vcs/src/store/hooks/compress.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/hooks/compress.ts rename to packages/insomnia-vcs/src/store/hooks/compress.ts diff --git a/packages/insomnia/src/main/cloud-sync/core/store/index.ts b/packages/insomnia-vcs/src/store/index.ts similarity index 100% rename from packages/insomnia/src/main/cloud-sync/core/store/index.ts rename to packages/insomnia-vcs/src/store/index.ts diff --git a/packages/insomnia/src/sync/types.ts b/packages/insomnia-vcs/src/types.ts similarity index 94% rename from packages/insomnia/src/sync/types.ts rename to packages/insomnia-vcs/src/types.ts index 01a091bac12a..60e45c0a3c81 100644 --- a/packages/insomnia/src/sync/types.ts +++ b/packages/insomnia-vcs/src/types.ts @@ -94,12 +94,7 @@ export interface StageEntryModify { export type StageEntry = StageEntryDelete | StageEntryAdd | StageEntryModify; -export const RESOLUTION_SOURCE = { - CHOOSE: 'choose', - MANUAL: 'manual', -} as const; - -export type ResolutionSource = (typeof RESOLUTION_SOURCE)[keyof typeof RESOLUTION_SOURCE]; +export type ResolutionSource = 'choose' | 'manual'; export interface MergeConflict { name: string; diff --git a/packages/insomnia/src/main/cloud-sync/core/util.ts b/packages/insomnia-vcs/src/util.ts similarity index 98% rename from packages/insomnia/src/main/cloud-sync/core/util.ts rename to packages/insomnia-vcs/src/util.ts index 072b54217782..c3d6f838c378 100644 --- a/packages/insomnia/src/main/cloud-sync/core/util.ts +++ b/packages/insomnia-vcs/src/util.ts @@ -2,9 +2,9 @@ import crypto from 'node:crypto'; import clone from 'clone'; import type { BaseModel } from 'insomnia-data'; +import { models } from 'insomnia-data'; +import { deterministicStringify } from 'insomnia-data/common'; -import { deleteKeys, resetKeys } from '../../../sync/ignore-keys'; -import { deterministicStringify } from '../../../sync/lib/deterministic-stringify'; import type { Branch, Compare, @@ -17,7 +17,7 @@ import type { StageEntry, StatusCandidate, StatusCandidateMap, -} from '../../../sync/types'; +} from './types'; export function generateSnapshotStateMap(snapshot: Snapshot | null): SnapshotStateMap { if (!snapshot) { @@ -455,8 +455,8 @@ export function hashDocument(doc?: BaseModel) { const newDoc = clone(doc); if (newDoc) { - deleteKeys(newDoc); - resetKeys(newDoc); + models.deleteKeys(newDoc); + models.resetKeys(newDoc); } return hash(newDoc); diff --git a/packages/insomnia/src/main/cloud-sync/core/vcs.ts b/packages/insomnia-vcs/src/vcs.ts similarity index 96% rename from packages/insomnia/src/main/cloud-sync/core/vcs.ts rename to packages/insomnia-vcs/src/vcs.ts index 1a9d3283a7fa..b3eb43986dbc 100644 --- a/packages/insomnia/src/main/cloud-sync/core/vcs.ts +++ b/packages/insomnia-vcs/src/vcs.ts @@ -6,15 +6,16 @@ import path from 'node:path'; import clone from 'clone'; import { runVcsGraphQL } from 'insomnia-api'; -import type { BaseModel } from 'insomnia-data'; +import type { BaseModel, UserSession } from 'insomnia-data'; +import type { Operation } from 'insomnia-data'; +import { services } from 'insomnia-data'; +import { deterministicStringify, generateId } from 'insomnia-data/common'; -import * as crypt from '~/common/account/crypt'; -import * as session from '~/common/account/session'; -import { PLAYWRIGHT_TEST } from '~/common/constants'; -import { deterministicStringify } from '~/sync/lib/deterministic-stringify'; - -import type { Operation } from '../../../common/database'; -import { generateId } from '../../../common/misc'; +import * as crypt from './crypt'; +import Store from './store'; +import type { BaseDriver } from './store/drivers/base'; +import FileSystemDriver from './store/drivers/file-system-driver'; +import compress from './store/hooks/compress'; import type { BackendProject, BackendProjectWithTeams, @@ -28,10 +29,7 @@ import type { Stage, StageEntry, StatusCandidate, -} from '../../../sync/types'; -import Store from './store'; -import type { BaseDriver } from './store/drivers/base'; -import compress from './store/hooks/compress'; +} from './types'; import { compareBranches, generateCandidateMap, @@ -86,10 +84,24 @@ const generateAES256KeyInNode = async (): Promise => { }; }; +export type VcsOptions = { + conflictHandler?: ConflictHandler; + testMode?: boolean; +} & ( + | { + driver?: undefined; + dataPath: string; + } + | { + driver: BaseDriver; + dataPath?: undefined; + } +); + // Stage/Unstage // Staged items are about to be committed // Unstaged items have changed compared to staged or not and can be staged -// +// VCS is only for cloud sync export class VCS { async getVersion(): Promise { const branch = await this._getCurrentBranch(); @@ -106,12 +118,14 @@ export class VCS { // stored by key `/projects/${project.id}/meta.json` _backendProject: BackendProject | null; _conflictHandler?: ConflictHandler | null; + _testMode: boolean | undefined; _stageByBackendProjectId: Record = {}; - constructor(driver: BaseDriver, conflictHandler?: ConflictHandler) { - this._store = new Store(driver, [compress]); + constructor({ driver, dataPath, conflictHandler, testMode }: VcsOptions) { + this._driver = driver || FileSystemDriver.create(dataPath!); + this._store = new Store(this._driver, [compress]); + this._testMode = testMode; this._conflictHandler = conflictHandler; - this._driver = driver; // To be set later this._backendProject = null; } @@ -480,6 +494,8 @@ export class VCS { }; } + // rename preMergeCheck to instance getCandidateStatus + async handleAnyConflicts( conflicts: MergeConflict[], labels: { ours: string; theirs: string }, @@ -1358,7 +1374,7 @@ export class VCS { async _getBackendProjectSymmetricKey() { const { privateKey, symmetricKey } = await this._assertSession(); - if (PLAYWRIGHT_TEST) { + if (this._testMode) { // use the session symmetric key in playwright tests return symmetricKey; } @@ -1368,6 +1384,22 @@ export class VCS { return JSON.parse(symmetricKeyStr); } + // TODO: This is a temporary solution to get the private key from the session. + async _getPrivateKey(sessionData?: UserSession): Promise { + if (!sessionData) { + throw new Error("Can't get private key: session is blank."); + } + + const { symmetricKey, encPrivateKey } = sessionData; + + if (!symmetricKey || !encPrivateKey) { + throw new Error("Can't get private key: session is missing keys."); + } + + const privateKeyStr = await crypt.decryptAES(symmetricKey, encPrivateKey); + return JSON.parse(privateKeyStr) as JsonWebKey; + } + async _assertBackendProject() { const project = await this._getBackendProject(); @@ -1411,8 +1443,10 @@ export class VCS { } async _assertSession() { - const { accountId, id, publicKey, symmetricKey } = await session.getUserSession(); - const privateKey = await session.getPrivateKey(); + const sessionData = await services.userSession.get(); + const { accountId, id, publicKey, symmetricKey } = sessionData; + const privateKey = await this._getPrivateKey(sessionData); + if (!id) { throw new Error('Not logged in'); } @@ -1560,21 +1594,25 @@ export class VCS { } async _assertSnapshot(id: string) { - const snapshot: Snapshot = await this._store.getItem(`/projects/${this._backendProjectId()}/snapshots/${id}.json`); - - if (snapshot && typeof snapshot.created === 'string') { - snapshot.created = new Date(snapshot.created); - } + const snapshot: Snapshot | null = await this._store.getItem( + `/projects/${this._backendProjectId()}/snapshots/${id}.json`, + ); if (!snapshot) { throw new Error(`Failed to find commit id=${id}`); } + if (snapshot && typeof snapshot.created === 'string') { + snapshot.created = new Date(snapshot.created); + } + return snapshot; } async _getSnapshot(id: string) { - const snapshot: Snapshot = await this._store.getItem(`/projects/${this._backendProjectId()}/snapshots/${id}.json`); + const snapshot: Snapshot | null = await this._store.getItem( + `/projects/${this._backendProjectId()}/snapshots/${id}.json`, + ); if (snapshot && typeof snapshot.created === 'string') { snapshot.created = new Date(snapshot.created); diff --git a/packages/insomnia-vcs/tsconfig.json b/packages/insomnia-vcs/tsconfig.json new file mode 100644 index 000000000000..7bc70ad9839d --- /dev/null +++ b/packages/insomnia-vcs/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2023", "WebWorker"], + "types": ["node"], + "target": "es2022", + "allowJs": false, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "noEmit": true, + "module": "ESNext", + "sourceMap": true, + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "useUnknownInCatchVariables": false, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./**/*.ts"] +} diff --git a/packages/insomnia-vcs/tsconfig.test.json b/packages/insomnia-vcs/tsconfig.test.json new file mode 100644 index 000000000000..8ce1d03d68cc --- /dev/null +++ b/packages/insomnia-vcs/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2023", "DOM"], + "types": ["node"] + }, + "include": ["**/*.test.ts", "setup-vitest.ts", "vitest.config.ts"] +} diff --git a/packages/insomnia-vcs/vitest.config.ts b/packages/insomnia-vcs/vitest.config.ts new file mode 100644 index 000000000000..7e7b5b6601e8 --- /dev/null +++ b/packages/insomnia-vcs/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { setupFiles: ['./setup-vitest.ts'], environment: 'node' }, +}); diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 8710e8e6f3fc..ef63e66556d6 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -137,7 +137,6 @@ "mocha": "^11.7.5", "monaco-editor": "^0.52.2", "multiparty": "^4.2.3", - "node-forge": "^1.3.1", "oauth-1.0a": "^2.2.6", "objectpath": "^2.0.0", "papaparse": "^5.5.2", @@ -166,7 +165,6 @@ "zod": "^3.25.75" }, "devDependencies": { - "@develohpanda/fluent-builder": "^2.1.2", "@modelcontextprotocol/sdk": "^1.17.5", "@react-router/dev": "7.18.2", "@tailwindcss/vite": "^4.1.17", @@ -192,7 +190,6 @@ "@types/multiparty": "^0.0.36", "@types/ncp": "^2.0.8", "@types/nedb": "^1.8.16", - "@types/node-forge": "^1.3.11", "@types/papaparse": "^5.3.15", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", diff --git a/packages/insomnia/src/__mocks__/node-forge.ts b/packages/insomnia/src/__mocks__/node-forge.ts deleted file mode 100644 index cee522ec3661..000000000000 --- a/packages/insomnia/src/__mocks__/node-forge.ts +++ /dev/null @@ -1,99 +0,0 @@ -import forge from 'node-forge'; - -// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. -module.exports = { - jsbn: forge.jsbn, - util: forge.util, - pkcs5: { - pbkdf2: forge.pkcs5.pbkdf2, - }, - md: { - sha256: forge.md.sha256, - }, - rsa: { - setPublicKey() { - return { - encrypt(str: string) { - return str; - }, - }; - }, - - setPrivateKey() { - return { - decrypt(str: string) { - return str; - }, - }; - }, - }, - random: { - getBytesSync(num: number) { - let s = ''; - - for (let i = 0; i < num; i++) { - s += 'a'; - } - - return s; - }, - }, - pki: { - rsa: { - generateKeyPair() { - return { - privateKey: { - d: 'a', - dP: 'a', - dQ: 'a', - e: 'a', - n: 'a', - p: 'a', - q: 'a', - qInv: 'a', - }, - publicKey: { - e: 'a', - n: 'a', - }, - }; - }, - }, - }, - cipher: { - createCipher() { - return { - start(config) { - this._config = config; - }, - - update(buffer) { - this._data = buffer; - }, - - finish() { - this.mode = { - tag: 'tag', - }; - this.output = this._data; - }, - }; - }, - - createDecipher() { - return { - start(config) { - this._config = config; - }, - - update(buffer) { - this.output = buffer; - }, - - finish() { - return true; - }, - }; - }, - }, -}; diff --git a/packages/insomnia/src/common/account/crypt.ts b/packages/insomnia/src/common/account/crypt.ts index 4a85806dd572..f457d1c26f0e 100644 --- a/packages/insomnia/src/common/account/crypt.ts +++ b/packages/insomnia/src/common/account/crypt.ts @@ -1,245 +1,13 @@ import type { AESMessage } from 'insomnia-data'; -import forge from 'node-forge'; export type { AESMessage }; -/** - * Encrypt with RSA256 public key - * - * @param publicKeyJWK - * @param plaintext - * @return String - */ -export function encryptRSAWithJWK(publicKeyJWK: JsonWebKey, plaintext: string) { - if (publicKeyJWK.alg !== 'RSA-OAEP-256') { - throw new Error('Public key algorithm was not RSA-OAEP-256'); - } else if (publicKeyJWK.kty !== 'RSA') { - throw new Error('Public key type was not RSA'); - } else if (!publicKeyJWK.key_ops || !publicKeyJWK.key_ops.find(o => o === 'encrypt')) { - throw new Error('Public key does not have "encrypt" op'); - } else if (!publicKeyJWK.n || !publicKeyJWK.e) { - throw new Error('Public key is missing parameters'); - } - - const encodedPlaintext = encodeURIComponent(plaintext); - - const n = _b64UrlToBigInt(publicKeyJWK.n); - - const e = _b64UrlToBigInt(publicKeyJWK.e); - - // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason - const publicKey = forge.rsa.setPublicKey(n, e); - const encrypted = publicKey.encrypt(encodedPlaintext, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - return forge.util.bytesToHex(encrypted); -} - -export function decryptRSAWithJWK(privateJWK: JsonWebKey, encryptedBlob: string) { - if ( - !privateJWK.n || - !privateJWK.e || - !privateJWK.d || - !privateJWK.p || - !privateJWK.q || - !privateJWK.dp || - !privateJWK.dq || - !privateJWK.qi - ) { - throw new Error('Private key is missing parameters'); - } - - const n = _b64UrlToBigInt(privateJWK.n); - const e = _b64UrlToBigInt(privateJWK.e); - const d = _b64UrlToBigInt(privateJWK.d); - const p = _b64UrlToBigInt(privateJWK.p); - const q = _b64UrlToBigInt(privateJWK.q); - const dP = _b64UrlToBigInt(privateJWK.dp); - const dQ = _b64UrlToBigInt(privateJWK.dq); - const qInv = _b64UrlToBigInt(privateJWK.qi); - - // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason - const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv); - const bytes = forge.util.hexToBytes(encryptedBlob); - const decrypted = privateKey.decrypt(bytes, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - return decodeURIComponent(decrypted); -} - -/** - * Encrypt data using symmetric key - * - * @param jwkOrKey JWK or string representing symmetric key - * @param buff data to encrypt - * @param additionalData any additional public data to attach - * @returns {{iv, t, d, ad}} - */ -export function encryptAESBuffer(jwkOrKey: string | JsonWebKey, buff: Buffer, additionalData = ''): AESMessage { - // TODO: Add assertion checks for JWK - const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); - const key = forge.util.hexToBytes(rawKey); - const iv = forge.random.getBytesSync(12); - const cipher = forge.cipher.createCipher('AES-GCM', key); - cipher.start({ - additionalData, - iv, - tagLength: 128, - }); - cipher.update(forge.util.createBuffer(buff)); - cipher.finish(); - return { - iv: forge.util.bytesToHex(iv), - // @ts-expect-error -- TSCONVERSION needs to be converted to string - t: forge.util.bytesToHex(cipher.mode.tag), - ad: forge.util.bytesToHex(additionalData), - // @ts-expect-error -- TSCONVERSION needs to be converted to string - d: forge.util.bytesToHex(cipher.output), - }; -} - -/** - * Encrypt data using symmetric key - * - * @param jwkOrKey JWK or string representing symmetric key - * @param plaintext string of data to encrypt - * @param additionalData any additional public data to attach - * @returns {{iv, t, d, ad}} - */ -export function encryptAES(jwkOrKey: string | JsonWebKey, plaintext: string, additionalData = ''): AESMessage { - // TODO: Add assertion checks for JWK - const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); - const key = forge.util.hexToBytes(rawKey); - const iv = forge.random.getBytesSync(12); - const cipher = forge.cipher.createCipher('AES-GCM', key); - // Plaintext could contain weird unicode, so we have to encode that - const encodedPlaintext = encodeURIComponent(plaintext); - cipher.start({ - additionalData, - iv, - tagLength: 128, - }); - cipher.update(forge.util.createBuffer(encodedPlaintext)); - cipher.finish(); - return { - iv: forge.util.bytesToHex(iv), - // @ts-expect-error -- TSCONVERSION needs to be converted to string - t: forge.util.bytesToHex(cipher.mode.tag), - ad: forge.util.bytesToHex(additionalData), - // @ts-expect-error -- TSCONVERSION needs to be converted to string - d: forge.util.bytesToHex(cipher.output), - }; -} - -/** - * Decrypt AES using a key - * - * @param jwkOrKey JWK or string representing symmetric key - * @param encryptedResult encryption data - * @returns String - */ -export function decryptAES(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) { - // TODO: Add assertion checks for JWK - const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); - const key = forge.util.hexToBytes(rawKey); - // ~~~~~~~~~~~~~~~~~~~~ // - // Decrypt with AES-GCM // - // ~~~~~~~~~~~~~~~~~~~~ // - const decipher = forge.cipher.createDecipher('AES-GCM', key); - decipher.start({ - iv: forge.util.hexToBytes(encryptedResult.iv), - tagLength: encryptedResult.t.length * 4, - // @ts-expect-error -- TSCONVERSION needs to be converted to string - tag: forge.util.hexToBytes(encryptedResult.t), - additionalData: forge.util.hexToBytes(encryptedResult.ad), - }); - decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); - - if (decipher.finish()) { - return decodeURIComponent(decipher.output.toString()); - } - throw new Error('Failed to decrypt data'); -} - -/** - * Decrypts AES using a key to buffer - * @param jwkOrKey - * @param encryptedResult - * @returns {string} - */ -export function decryptAESToBuffer(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) { - // TODO: Add assertion checks for JWK - const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); - const key = forge.util.hexToBytes(rawKey); - // ~~~~~~~~~~~~~~~~~~~~ // - // Decrypt with AES-GCM // - // ~~~~~~~~~~~~~~~~~~~~ // - const decipher = forge.cipher.createDecipher('AES-GCM', key); - decipher.start({ - iv: forge.util.hexToBytes(encryptedResult.iv), - tagLength: encryptedResult.t.length * 4, - // @ts-expect-error -- TSCONVERSION needs to be converted to string - tag: forge.util.hexToBytes(encryptedResult.t), - additionalData: forge.util.hexToBytes(encryptedResult.ad), - }); - decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); - - if (decipher.finish()) { - // @ts-expect-error -- TSCONVERSION needs to be converted to string - return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex'); - } - throw new Error('Failed to decrypt data'); -} - -/** - * Generate a random AES256 key for use with symmetric encryption - */ -export async function generateAES256Key() { - // globalThis.crypto resolves to WebCrypto in the renderer and Node global Web Crypto - // in the main process, keeping this module isomorphic. - const c = globalThis.crypto; - // @ts-expect-error -- TSCONVERSION: likely needs a module augmentation for webkit - const subtle = c ? c.subtle || c.webkitSubtle : null; - - if (subtle) { - console.log('[crypt] Using Native AES Key Generation'); - const key = await subtle.generateKey( - { - name: 'AES-GCM', - length: 256, - }, - true, - ['encrypt', 'decrypt'], - ); - return subtle.exportKey('jwk', key); - } - console.log('[crypt] Using Fallback Forge AES Key Generation'); - const key = forge.util.bytesToHex(forge.random.getBytesSync(32)); - return { - kty: 'oct', - alg: 'A256GCM', - ext: true, - key_ops: ['encrypt', 'decrypt'], - k: _hexToB64Url(key), - }; -} - -// ~~~~~~~~~~~~~~~~ // -// Helper Functions // -// ~~~~~~~~~~~~~~~~ // - -function _hexToB64Url(h: string) { - const bytes = forge.util.hexToBytes(h); - return btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); -} - -function _b64UrlToBigInt(s: string) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- see below - // @ts-ignore -- unfortunately, we must ignore here instead of the usual expect-error because this mondule is being used by two different builds (`insomnia` and `insomnia-send-request`) and in one of them this line is an error (`insomnia-send-request`) and the other it is not ()`insomnia`). - return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16); -} - -function _b64UrlToHex(s: string) { - const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); - return forge.util.bytesToHex(atob(b64)); -} +export { + encryptRSAWithJWK, + decryptRSAWithJWK, + encryptAESBuffer, + encryptAES, + decryptAES, + decryptAESToBuffer, + generateAES256Key, +} from 'insomnia-vcs'; diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index b520ccba5154..407f50602cfe 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -15,7 +15,7 @@ import { invariant } from '~/common/utils/invariant'; import { invokeWithNormalizedError } from '~/main/ipc/invoke'; import type { LLMBackend, LLMConfig, LLMConfigServiceAPI } from '~/main/llm-config-service'; import type { PluginInvokeMethod } from '~/plugins/invoke-method'; -import { isUserAbortResolveMergeConflictError, UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; +import { isUserAbortResolveMergeConflictError, UserAbortResolveMergeConflictError } from '~/sync/vcs/utils'; import { servicesProxy } from '~/ui/renderer-services-proxy'; import type { SyncBridgeAPI } from './main/cloud-sync/ipc'; diff --git a/packages/insomnia/src/main/cloud-sync/create-vcs.ts b/packages/insomnia/src/main/cloud-sync/create-vcs.ts deleted file mode 100644 index e27959c2a4e9..000000000000 --- a/packages/insomnia/src/main/cloud-sync/create-vcs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MergeConflict } from '../../sync/types'; -import FileSystemDriver from './core/store/drivers/file-system-driver'; -import { VCS } from './core/vcs'; - -export type ConflictHandler = ( - conflicts: MergeConflict[], - labels: { ours: string; theirs: string }, -) => Promise; - -export const createVCS = ({ dataPath, conflictHandler }: { dataPath: string; conflictHandler?: ConflictHandler }) => { - return new VCS(FileSystemDriver.create(dataPath), conflictHandler); -}; diff --git a/packages/insomnia/src/main/cloud-sync/ipc.ts b/packages/insomnia/src/main/cloud-sync/ipc.ts index 651bd89aabc5..855ea66f39a6 100644 --- a/packages/insomnia/src/main/cloud-sync/ipc.ts +++ b/packages/insomnia/src/main/cloud-sync/ipc.ts @@ -9,7 +9,7 @@ import type { StageEntry, Status, StatusCandidate, -} from '~/sync/types'; +} from 'insomnia-vcs'; import type { Operation } from '../../common/database'; import { ipcMainHandle, ipcMainOn } from '../ipc/electron'; diff --git a/packages/insomnia/src/main/cloud-sync/pull-backend-project.ts b/packages/insomnia/src/main/cloud-sync/pull-backend-project.ts index 63e8c0e915af..576cf31af6e4 100644 --- a/packages/insomnia/src/main/cloud-sync/pull-backend-project.ts +++ b/packages/insomnia/src/main/cloud-sync/pull-backend-project.ts @@ -1,9 +1,8 @@ import type { RemoteProject } from 'insomnia-data'; import { database, models, services } from 'insomnia-data'; +import type { BackendProjectWithTeam, VCS } from 'insomnia-vcs'; -import type { VCS } from '~/main/cloud-sync/core/vcs'; import { interceptAccessError } from '~/sync/access-error'; -import type { BackendProjectWithTeam } from '~/sync/types'; import { DEFAULT_BRANCH_NAME } from '../../common/constants'; diff --git a/packages/insomnia/src/main/cloud-sync/vcs.ts b/packages/insomnia/src/main/cloud-sync/vcs.ts index 7ceb322d9a05..e35e002df9cb 100644 --- a/packages/insomnia/src/main/cloud-sync/vcs.ts +++ b/packages/insomnia/src/main/cloud-sync/vcs.ts @@ -4,13 +4,13 @@ import { randomUUID } from 'node:crypto'; import { app, type WebContents } from 'electron'; import type { RemoteProject } from 'insomnia-data'; import { services } from 'insomnia-data'; +import type { BackendProjectWithTeam, MergeConflict } from 'insomnia-vcs'; +import { VCS } from 'insomnia-vcs'; +import { PLAYWRIGHT_TEST } from '~/common/constants'; import { invariant } from '~/common/utils/invariant'; -import type { VCS } from '~/main/cloud-sync/core/vcs'; -import { createVCS } from '~/main/cloud-sync/create-vcs'; import { pullBackendProject } from '~/main/cloud-sync/pull-backend-project'; -import type { BackendProjectWithTeam, MergeConflict } from '~/sync/types'; -import { UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; +import { UserAbortResolveMergeConflictError } from '~/sync/vcs/utils'; interface SyncInvocationContext { sender: WebContents; @@ -53,16 +53,19 @@ const requestConflictResolution = (conflicts: MergeConflict[], labels: { ours: s }); }; -export const getMainVCS = () => { - if (mainVCS) { - return mainVCS; - } - - mainVCS = createVCS({ +function createVCS() { + return new VCS({ dataPath: process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'), conflictHandler: requestConflictResolution, + testMode: !!PLAYWRIGHT_TEST, }); +} +export const getMainVCS = () => { + if (mainVCS) { + return mainVCS; + } + mainVCS = createVCS(); return mainVCS; }; @@ -136,10 +139,7 @@ export const pullRemoteBackendProjectWithSingleton = async ( const project = await services.project.getByRemoteId(remoteId); invariant(project?.remoteId, 'Project is not a remote project'); - const pullVCS = createVCS({ - dataPath: process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'), - conflictHandler: requestConflictResolution, - }); + const pullVCS = createVCS(); await pullVCS.removeBackendProjectsForRoot(backendProject.rootDocumentId); const { workspaceId } = await pullBackendProject({ diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts index b83db8bd730f..cd2def8b900c 100644 --- a/packages/insomnia/src/main/git-service.ts +++ b/packages/insomnia/src/main/git-service.ts @@ -29,6 +29,7 @@ import type { } from 'insomnia-data'; import { models, services } from 'insomnia-data'; import { slugify } from 'insomnia-data/common'; +import type { AutoResolvedConflict, MergeConflict } from 'insomnia-vcs'; import { Errors, type PromiseFsClient } from 'isomorphic-git'; import YAML, { parse } from 'yaml'; @@ -67,7 +68,6 @@ import { projectRoutableFSClient } from '../sync/git/project-routable-fs-client' import { RepoFileWatcherRegistry, type WatcherNotifier } from '../sync/git/repo-file-watcher'; import { routableFSClient } from '../sync/git/routable-fs-client'; import { shallowClone } from '../sync/git/shallow-clone'; -import type { AutoResolvedConflict, MergeConflict } from '../sync/types'; import { AnalyticsEvent, trackAnalyticsEvent } from './analytics'; import { ipcMainHandle } from './ipc/electron'; @@ -425,7 +425,11 @@ function resolveWithinGitRoot(gitRoot: string, folderName: string): string | nul * the GitRepository document should pass `directory`/`folderSlug` to avoid a DB * lookup; otherwise both are resolved from the database by id. */ -async function getRepoBaseDir(gitRepositoryId: string, directory?: string | null, folderSlug?: string | null): Promise { +async function getRepoBaseDir( + gitRepositoryId: string, + directory?: string | null, + folderSlug?: string | null, +): Promise { let dir = directory; let slug = folderSlug; if (dir === undefined) { @@ -517,7 +521,10 @@ async function backfillManagedFolderSlug(repo: GitRepository, projectId: string) const gitRoot = getManagedGitRoot(); const oldDir = resolveWithinGitRoot(gitRoot, models.gitRepository.getGitRepoFolderName(repo)); - const newDir = resolveWithinGitRoot(gitRoot, models.gitRepository.getGitRepoFolderName({ _id: repo._id, folderSlug: slug })); + const newDir = resolveWithinGitRoot( + gitRoot, + models.gitRepository.getGitRepoFolderName({ _id: repo._id, folderSlug: slug }), + ); if (!oldDir || !newDir) { // Should be unreachable — getGitRepoFolderName already validates folderSlug — but // never rename into/out of a path outside gitRoot. diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx index e7f52422dfcb..dd36afa63c84 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx @@ -3,7 +3,7 @@ import { href } from 'react-router'; import type { Operation } from '~/common/database'; import { database } from '~/common/database'; import { invariant } from '~/common/utils/invariant'; -import { UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; +import { UserAbortResolveMergeConflictError } from '~/sync/vcs/utils'; import { getSyncItems, remoteCompareCache, reparentSyncDelta } from '~/ui/sync-utils'; import { createFetcherSubmitHook } from '~/ui/utils/router'; diff --git a/packages/insomnia/src/sync/git/git-vcs.ts b/packages/insomnia/src/sync/git/git-vcs.ts index 5cfb5cf7df26..301213dd2cd5 100644 --- a/packages/insomnia/src/sync/git/git-vcs.ts +++ b/packages/insomnia/src/sync/git/git-vcs.ts @@ -3,15 +3,16 @@ import path from 'node:path'; import type { Change } from 'diff'; import { diffLines } from 'diff'; import type { GitAuthor, GitRemoteConfig } from 'insomnia-data'; +import { type AutoResolvedConflict, type MergeConflict } from 'insomnia-vcs'; import * as git from 'isomorphic-git'; import { parse, stringify } from 'yaml'; import { migrateToLatestYaml } from '~/common/insomnia-schema-migrations'; import { GitVCSOperationErrors } from '~/sync/git/git-vcs-operation-errors'; import type { WriteFileMap } from '~/sync/git/project-routable-fs-client'; +import { RESOLUTION_SOURCE } from '~/sync/vcs/utils'; import { hasSignificantChanges } from '../../common/significant-diff-detection'; -import { type AutoResolvedConflict, type MergeConflict, RESOLUTION_SOURCE } from '../types'; import { httpClient } from './http-client'; import { convertToPosixSep } from './path-sep'; import { getAuthorFromGitRepository, gitCallbacks } from './utils'; diff --git a/packages/insomnia/src/sync/git/ne-db-client.ts b/packages/insomnia/src/sync/git/ne-db-client.ts index 1d77c5c69761..ec2384feaa64 100644 --- a/packages/insomnia/src/sync/git/ne-db-client.ts +++ b/packages/insomnia/src/sync/git/ne-db-client.ts @@ -21,7 +21,6 @@ import type { PromiseFsClient } from 'isomorphic-git'; import YAML from 'yaml'; import { database as db } from '../../common/database'; -import { resetKeys } from '../ignore-keys'; import { GIT_INSOMNIA_DIR_NAME } from './git-vcs'; import parseGitPath from './parse-git-path'; import Stat from './stat'; @@ -85,7 +84,7 @@ export class NeDBClient { } // When git is reading from NeDb, reset keys we wish to ignore to their original values - resetKeys(doc); + models.resetKeys(doc); // It would be nice to be able to add this check here but we can't since // isomorphic-git may have just deleted the workspace from the FS. This diff --git a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts index bef659769771..68e0715dba36 100644 --- a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts +++ b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts @@ -1,8 +1,8 @@ import type { BaseModel, Project, Workspace } from 'insomnia-data'; import { models, services } from 'insomnia-data'; +import type { Stage, StageEntry, Status, StatusCandidate } from 'insomnia-vcs'; import { database } from '../../common/database'; -import type { Stage, StageEntry, Status, StatusCandidate } from '../types'; export interface SyncVCSLike { hasBackendProject: () => boolean | Promise; diff --git a/packages/insomnia/src/sync/vcs/errors.ts b/packages/insomnia/src/sync/vcs/utils.ts similarity index 63% rename from packages/insomnia/src/sync/vcs/errors.ts rename to packages/insomnia/src/sync/vcs/utils.ts index 1d3b8a61ed3b..7604567b3a23 100644 --- a/packages/insomnia/src/sync/vcs/errors.ts +++ b/packages/insomnia/src/sync/vcs/utils.ts @@ -1,3 +1,5 @@ +import type { ResolutionSource } from 'insomnia-vcs'; + export class UserAbortResolveMergeConflictError extends Error { constructor(message = 'User aborted merge') { super(message); @@ -8,3 +10,12 @@ export class UserAbortResolveMergeConflictError extends Error { export const isUserAbortResolveMergeConflictError = (error: unknown): error is UserAbortResolveMergeConflictError => typeof error === 'object' && error !== null && 'name' in error && error.name === 'UserAbortResolveMergeConflictError'; + +type UnionToObject = { + [K in T as Uppercase]: K; +}; + +export const RESOLUTION_SOURCE: UnionToObject = { + CHOOSE: 'choose', + MANUAL: 'manual', +}; diff --git a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx index cd93ca4cf114..527600c2098b 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx @@ -1,5 +1,6 @@ import type { IconName, IconProp } from '@fortawesome/fontawesome-svg-core'; import type { GitProject, GitRepository } from 'insomnia-data'; +import type { MergeConflict } from 'insomnia-vcs'; import { type FC, useEffect, useMemo, useRef, useState } from 'react'; import { Button, @@ -29,7 +30,6 @@ import { useGitCredentials } from '~/ui/hooks/use-git-credentials'; import { useOrganizationStorageRule } from '~/ui/hooks/use-organization-storage-rule'; import { resolveGitRepoBaseDir } from '~/ui/utils/git-repo-path'; -import type { MergeConflict } from '../../../sync/types'; import { GitNonOriginBranchBanner } from '../git/git-non-origin-branch-banner'; import { Icon } from '../icon'; import { showModal } from '../modals'; diff --git a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx index b9ba6d153e17..5319891264aa 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx @@ -1,5 +1,6 @@ import type { IconName, IconProp } from '@fortawesome/fontawesome-svg-core'; import type { GitRepository } from 'insomnia-data'; +import type { MergeConflict } from 'insomnia-vcs'; import { type FC, useEffect, useState } from 'react'; import { Button, @@ -23,7 +24,6 @@ import { useGitProjectResetActionFetcher } from '~/routes/git.reset'; import { useGitProjectStatusActionFetcher } from '~/routes/git.status'; import { getOauth2FormatName } from '~/sync/git/get-oauth2-format-name'; -import type { MergeConflict } from '../../../sync/types'; import { ConfigLink } from '../github-app-config-link'; import { Icon } from '../icon'; import { showModal } from '../modals'; diff --git a/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx index cc9d13055f5c..a6be2a49f160 100644 --- a/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx @@ -1,3 +1,4 @@ +import type { MergeConflict } from 'insomnia-vcs'; import { type FC, useEffect, useState } from 'react'; import { Button, @@ -19,7 +20,6 @@ import { useGitProjectNewBranchActionFetcher } from '~/routes/git.branch.new'; import { useGitProjectBranchesLoaderFetcher } from '~/routes/git.branches'; import { useGitProjectChangesFetcher } from '~/routes/git.changes'; -import type { MergeConflict } from '../../../sync/types'; import { PromptButton } from '../base/prompt-button'; import { Icon } from '../icon'; import { AlertModal } from './alert-modal'; diff --git a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx index 50a1ae5b3d10..a3f1ddce83ae 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx @@ -1,3 +1,4 @@ +import type { MergeConflict } from 'insomnia-vcs'; import { type FC, useEffect, useState } from 'react'; import { Button, @@ -18,7 +19,6 @@ import { useGitProjectDeleteBranchActionFetcher } from '~/routes/git.branch.dele import { useGitProjectNewBranchActionFetcher } from '~/routes/git.branch.new'; import { useGitProjectBranchesLoaderFetcher } from '~/routes/git.branches'; import { useGitProjectChangesFetcher } from '~/routes/git.changes'; -import type { MergeConflict } from '~/sync/types'; import { SyncMergeModal } from '~/ui/components/modals/sync-merge-modal'; import { PromptButton } from '../base/prompt-button'; diff --git a/packages/insomnia/src/ui/components/modals/sync-history-modal.tsx b/packages/insomnia/src/ui/components/modals/sync-history-modal.tsx index 92fb6ed1fcef..f57ddc9b0b0d 100644 --- a/packages/insomnia/src/ui/components/modals/sync-history-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/sync-history-modal.tsx @@ -1,3 +1,4 @@ +import type { Snapshot } from 'insomnia-vcs'; import { Button, Cell, @@ -16,7 +17,6 @@ import { useParams } from 'react-router'; import { useRootLoaderData } from '~/root'; import { useInsomniaSyncRestoreActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore'; -import type { Snapshot } from '../../../sync/types'; import { PromptButton } from '../base/prompt-button'; import { HelpTooltip } from '../help-tooltip'; import { Icon } from '../icon'; diff --git a/packages/insomnia/src/ui/components/modals/sync-merge-modal.tsx b/packages/insomnia/src/ui/components/modals/sync-merge-modal.tsx index 0aae11fb4f20..6854ed305ae6 100644 --- a/packages/insomnia/src/ui/components/modals/sync-merge-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/sync-merge-modal.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import type { MergeConflict } from 'insomnia-vcs'; import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { Button, @@ -16,10 +17,10 @@ import { parse, stringify } from 'yaml'; import { extractErrorMessages } from '~/common/import'; import { InsomniaFileSchema } from '~/common/import-v5-parser'; import { migrateToLatestYaml } from '~/common/insomnia-schema-migrations'; +import { RESOLUTION_SOURCE } from '~/sync/vcs/utils'; import { showModal } from '~/ui/components/modals'; import { AlertModal } from '~/ui/components/modals/alert-modal'; -import { type MergeConflict, RESOLUTION_SOURCE } from '../../../sync/types'; import { AnalyticsEvent } from '../../analytics'; import { MergeEditor } from '../.client/codemirror/merge-editor'; import { DiffEditor } from '../diff-view-editor'; diff --git a/packages/insomnia/src/ui/components/modals/sync-staging-modal.tsx b/packages/insomnia/src/ui/components/modals/sync-staging-modal.tsx index c12a9bd8a22a..d1fc46c94cb1 100644 --- a/packages/insomnia/src/ui/components/modals/sync-staging-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/sync-staging-modal.tsx @@ -1,4 +1,5 @@ import { models } from 'insomnia-data'; +import type { StageEntry, Status, StatusCandidate } from 'insomnia-vcs'; import { useEffect, useState } from 'react'; import { Button, @@ -21,7 +22,6 @@ import { useInsomniaSyncCreateSnapshotActionFetcher } from '~/routes/organizatio import { useInsomniaSyncStageActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage'; import { useInsomniaSyncUnstageActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage'; -import type { StageEntry, Status, StatusCandidate } from '../../../sync/types'; import { DiffEditor } from '../diff-view-editor'; import { Icon } from '../icon'; diff --git a/packages/insomnia/src/ui/sync-utils.ts b/packages/insomnia/src/ui/sync-utils.ts index e221b357fde2..343935dc2e72 100644 --- a/packages/insomnia/src/ui/sync-utils.ts +++ b/packages/insomnia/src/ui/sync-utils.ts @@ -15,10 +15,10 @@ import type { Workspace, } from 'insomnia-data'; import { models, services } from 'insomnia-data'; +import type { BackendProject, Compare, StatusCandidate } from 'insomnia-vcs'; import { database, type Operation } from '~/common/database'; import { invariant } from '~/common/utils/invariant'; -import type { BackendProject, Compare, StatusCandidate } from '~/sync/types'; type PushPull = 'push' | 'pull'; type VCSAction = diff --git a/packages/insomnia/src/ui/utils/__tests__/insomnia-sync.test.ts b/packages/insomnia/src/ui/utils/__tests__/insomnia-sync.test.ts index 725c845ac07b..6f49cbab53d5 100644 --- a/packages/insomnia/src/ui/utils/__tests__/insomnia-sync.test.ts +++ b/packages/insomnia/src/ui/utils/__tests__/insomnia-sync.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; +import { UserAbortResolveMergeConflictError } from '~/sync/vcs/utils'; vi.mock('~/ui/components/modals', () => ({ showModal: vi.fn(), diff --git a/packages/insomnia/src/ui/utils/insomnia-sync.ts b/packages/insomnia/src/ui/utils/insomnia-sync.ts index 58b5f3fd597d..177ce1e7602a 100644 --- a/packages/insomnia/src/ui/utils/insomnia-sync.ts +++ b/packages/insomnia/src/ui/utils/insomnia-sync.ts @@ -1,11 +1,11 @@ -import type { MergeConflict } from '~/sync/types'; +import type { MergeConflict } from 'insomnia-vcs'; import { showModal } from '../components/modals'; import { SyncMergeModal } from '../components/modals/sync-merge-modal'; let hasRegisteredConflictListener = false; -export { UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; +export { UserAbortResolveMergeConflictError } from '~/sync/vcs/utils'; export const registerSyncMergeConflictListener = () => { if (hasRegisteredConflictListener) { diff --git a/packages/insomnia/src/ui/utils/remote-projects.ts b/packages/insomnia/src/ui/utils/remote-projects.ts index 68516ae415e6..d2d2d182162d 100644 --- a/packages/insomnia/src/ui/utils/remote-projects.ts +++ b/packages/insomnia/src/ui/utils/remote-projects.ts @@ -1,7 +1,7 @@ import type { Project } from 'insomnia-data'; +import type { BackendProjectWithTeamsAndTeamProjectId } from 'insomnia-vcs'; import { type InsomniaFile } from '~/common/project'; -import type { BackendProjectWithTeamsAndTeamProjectId } from '~/sync/types'; export const getAllRemoteBackendProjectsOfOrg = async ({ organizationId }: { organizationId: string }) => { return window.main.sync.remoteBackendProjectsOfTeam({ teamId: organizationId }); diff --git a/packages/insomnia/types/node-forge-lib.d.ts b/packages/insomnia/types/node-forge-lib.d.ts deleted file mode 100644 index 51515a606d7c..000000000000 --- a/packages/insomnia/types/node-forge-lib.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module 'node-forge/lib/forge' { - import forge from 'node-forge'; - - export default forge; -} - -declare module 'node-forge/lib/util'; -declare module 'node-forge/lib/cipher'; -declare module 'node-forge/lib/cipherModes'; -declare module 'node-forge/lib/aes';