diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index d0329b81..59d84ef4 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -268,13 +268,6 @@ declare module 'vue' { ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default'] ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default'] InstanceSwitcher: typeof import('./components/instance/Switcher.vue')['default'] - InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default'] - InterceptorsAgentModalNativeClientCertificates: typeof import('./components/interceptors/agent/ModalNativeClientCertificates.vue')['default'] - InterceptorsAgentModalNativeClientCertsAdd: typeof import('./components/interceptors/agent/ModalNativeClientCertsAdd.vue')['default'] - InterceptorsAgentRegistrationModal: typeof import('./components/interceptors/agent/RegistrationModal.vue')['default'] - InterceptorsAgentRootExt: typeof import('./components/interceptors/agent/RootExt.vue')['default'] - InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default'] - InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default'] @@ -305,6 +298,7 @@ declare module 'vue' { SettingsAgentSubtitle: typeof import('./components/settings/AgentSubtitle.vue')['default'] SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] SettingsExtensionSubtitle: typeof import('./components/settings/ExtensionSubtitle.vue')['default'] + SettingsInterceptorErrorPlaceholder: typeof import('./components/settings/InterceptorErrorPlaceholder.vue')['default'] SettingsNative: typeof import('./components/settings/Native.vue')['default'] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] Share: typeof import('./components/share/index.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/app/spotlight/index.vue b/packages/hoppscotch-common/src/components/app/spotlight/index.vue index e731b29c..285d9daa 100644 --- a/packages/hoppscotch-common/src/components/app/spotlight/index.vue +++ b/packages/hoppscotch-common/src/components/app/spotlight/index.vue @@ -104,8 +104,6 @@ import { } from "~/services/spotlight/searchers/environment.searcher" import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher" import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher" -// NOTE: Old interceptors -// import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher" import { KernelInterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/kernel-interceptor.searcher" import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher" import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher" @@ -146,8 +144,6 @@ useService(EnvironmentsSpotlightSearcherService) useService(SwitchEnvSpotlightSearcherService) useService(WorkspaceSpotlightSearcherService) useService(SwitchWorkspaceSpotlightSearcherService) -// NOTE: Old interceptors -// useService(InterceptorSpotlightSearcherService) useService(KernelInterceptorSpotlightSearcherService) useService(TeamsSpotlightSearcherService) diff --git a/packages/hoppscotch-common/src/components/interceptors/ExtensionSubtitle.vue b/packages/hoppscotch-common/src/components/interceptors/ExtensionSubtitle.vue deleted file mode 100644 index 0b13f73e..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/ExtensionSubtitle.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeCACertificates.vue b/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeCACertificates.vue deleted file mode 100644 index 98489c4b..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeCACertificates.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertificates.vue b/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertificates.vue deleted file mode 100644 index f067f7fe..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertificates.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertsAdd.vue b/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertsAdd.vue deleted file mode 100644 index 16c12683..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/agent/ModalNativeClientCertsAdd.vue +++ /dev/null @@ -1,288 +0,0 @@ - - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/agent/RegistrationModal.vue b/packages/hoppscotch-common/src/components/interceptors/agent/RegistrationModal.vue deleted file mode 100644 index 4b01808b..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/agent/RegistrationModal.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/agent/RootExt.vue b/packages/hoppscotch-common/src/components/interceptors/agent/RootExt.vue deleted file mode 100644 index b39a360c..00000000 --- a/packages/hoppscotch-common/src/components/interceptors/agent/RootExt.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/packages/hoppscotch-common/src/components/interceptors/ErrorPlaceholder.vue b/packages/hoppscotch-common/src/components/settings/InterceptorErrorPlaceholder.vue similarity index 79% rename from packages/hoppscotch-common/src/components/interceptors/ErrorPlaceholder.vue rename to packages/hoppscotch-common/src/components/settings/InterceptorErrorPlaceholder.vue index 351ad840..6818b1c9 100644 --- a/packages/hoppscotch-common/src/components/interceptors/ErrorPlaceholder.vue +++ b/packages/hoppscotch-common/src/components/settings/InterceptorErrorPlaceholder.vue @@ -59,36 +59,31 @@ import IconChrome from "~icons/brands/chrome" import IconFirefox from "~icons/brands/firefox" import IconCheckCircle from "~icons/lucide/check-circle" import { useI18n } from "@composables/i18n" -import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension" +import { useColorMode } from "@composables/theming" import { useService } from "dioc/vue" import { computed } from "vue" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" +import { ExtensionKernelInterceptorService } from "~/platform/std/kernel-interceptors/extension" import { platform } from "~/platform" -import { useColorMode } from "~/composables/theming" const colorMode = useColorMode() const t = useI18n() -const interceptorService = useService(InterceptorService) -const extensionService = useService(ExtensionInterceptorService) +const kernelInterceptorService = useService(KernelInterceptorService) +const extensionService = useService(ExtensionKernelInterceptorService) const hasChromeExtInstalled = extensionService.chromeExtensionInstalled const hasFirefoxExtInstalled = extensionService.firefoxExtensionInstalled const extensionEnabled = computed({ get() { - return ( - interceptorService.currentInterceptorID.value === - extensionService.interceptorID - ) + return kernelInterceptorService.current.value?.id === extensionService.id }, set(active) { if (active) { - interceptorService.currentInterceptorID.value = - extensionService.interceptorID + kernelInterceptorService.setActive(extensionService.id) } else { - interceptorService.currentInterceptorID.value = - platform.interceptors.default + kernelInterceptorService.setActive(platform.kernelInterceptors.default) } }, }) diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts index 77f09ce7..1d4f639c 100644 --- a/packages/hoppscotch-common/src/helpers/actions.ts +++ b/packages/hoppscotch-common/src/helpers/actions.ts @@ -123,7 +123,6 @@ export type HoppAction = | "share.request" // Share REST request | "tab.duplicate-tab" // Duplicate REST request | "gql.request.open" // Open GraphQL request - | "agent.open-registration-modal" // Open Hoppscotch Agent registration modal | "app.quit" // Quit app /** diff --git a/packages/hoppscotch-common/src/modules/interceptors.ts b/packages/hoppscotch-common/src/modules/interceptors.ts deleted file mode 100644 index 28670296..00000000 --- a/packages/hoppscotch-common/src/modules/interceptors.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { InterceptorService } from "~/services/interceptor.service" -import { HoppModule } from "." -import { getService } from "./dioc" -import { platform } from "~/platform" -import { watch } from "vue" -import { applySetting } from "~/newstore/settings" -import { useSettingStatic } from "~/composables/settings" - -export default { - deprecated: true, - onVueAppInit() { - const interceptorService = getService(InterceptorService) - - for (const interceptorDef of platform.interceptors.interceptors) { - if (interceptorDef.type === "standalone") { - interceptorService.registerInterceptor(interceptorDef.interceptor) - } else { - const service = getService(interceptorDef.service) - - interceptorService.registerInterceptor(service) - } - } - - interceptorService.currentInterceptorID.value = - platform.interceptors.default - - watch(interceptorService.currentInterceptorID, (id) => { - applySetting( - "CURRENT_INTERCEPTOR_ID", - id ?? platform.interceptors.default - ) - }) - - const [setting] = useSettingStatic("CURRENT_INTERCEPTOR_ID") - - watch( - setting, - () => { - interceptorService.currentInterceptorID.value = - setting.value ?? platform.interceptors.default - }, - { - immediate: true, - } - ) - }, -} diff --git a/packages/hoppscotch-common/src/newstore/settings.ts b/packages/hoppscotch-common/src/newstore/settings.ts index af49be21..bdfa4458 100644 --- a/packages/hoppscotch-common/src/newstore/settings.ts +++ b/packages/hoppscotch-common/src/newstore/settings.ts @@ -54,7 +54,6 @@ export type SettingsDef = { multipartFormdata: boolean } - CURRENT_INTERCEPTOR_ID: string CURRENT_KERNEL_INTERCEPTOR_ID: string URL_EXCLUDES: { @@ -121,7 +120,6 @@ export const getDefaultSettings = (): SettingsDef => { }, // Set empty because interceptor module will set the default value - CURRENT_INTERCEPTOR_ID: "", CURRENT_KERNEL_INTERCEPTOR_ID: "", // TODO: Interceptor related settings should move under the interceptor systems @@ -317,19 +315,22 @@ export function performSettingsDataMigrations(data: any): SettingsDef { const source = cloneDeep(data) if (source["EXTENSIONS_ENABLED"]) { - const result = JSON.parse(source["EXTENSIONS_ENABLED"]) - - if (result) source["CURRENT_INTERCEPTOR_ID"] = "extension" delete source["EXTENSIONS_ENABLED"] } if (source["PROXY_ENABLED"]) { - const result = JSON.parse(source["PROXY_ENABLED"]) - - if (result) source["CURRENT_INTERCEPTOR_ID"] = "proxy" delete source["PROXY_ENABLED"] } + // Remove legacy interceptor ID if present, + // NOTE: These are not for `kernel` interceptors, + // those now don't participate in global settings, + // rather each has its own `store.ts` that can be + // controlled independently. + if (has(source, "CURRENT_INTERCEPTOR_ID")) { + delete source["CURRENT_INTERCEPTOR_ID"] + } + const final = defaultsDeep(source, getDefaultSettings()) return final diff --git a/packages/hoppscotch-common/src/pages/settings.vue b/packages/hoppscotch-common/src/pages/settings.vue index 92fc2efe..091a5abe 100644 --- a/packages/hoppscotch-common/src/pages/settings.vue +++ b/packages/hoppscotch-common/src/pages/settings.vue @@ -225,33 +225,6 @@ - -

@@ -313,8 +286,6 @@ import { useI18n } from "@composables/i18n" import { useColorMode } from "@composables/theming" import { usePageHead } from "@composables/head" import { useService } from "dioc/vue" -// NOTE: Old interceptors -// import { InterceptorService } from "~/services/interceptor.service" import { pipe } from "fp-ts/function" import * as O from "fp-ts/Option" import * as A from "fp-ts/Array" @@ -346,22 +317,6 @@ const kernelInterceptorsWithSettings = computed(() => ) ) -// NOTE: Old interceptors -// const interceptorService: InterceptorService = useService(InterceptorService) -// const interceptorsWithSettings = computed(() => -// pipe( -// interceptorService.availableInterceptors.value, -// A.filterMap((interceptor) => -// interceptor.settingsPageEntry -// ? O.some([ -// interceptor.interceptorID, -// interceptor.settingsPageEntry, -// ] as const) -// : O.none -// ) -// ) -// ) - const ACCENT_COLOR = useSetting("THEME_COLOR") const TELEMETRY_ENABLED = useSetting("TELEMETRY_ENABLED") const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") diff --git a/packages/hoppscotch-common/src/platform/index.ts b/packages/hoppscotch-common/src/platform/index.ts index bbd21924..873f5709 100644 --- a/packages/hoppscotch-common/src/platform/index.ts +++ b/packages/hoppscotch-common/src/platform/index.ts @@ -10,9 +10,6 @@ import { HistoryPlatformDef } from "./history" import { InfraPlatformDef } from "./infra" import { InspectorsPlatformDef } from "./inspectors" import { KernelInterceptorsPlatformDef } from "./kernel-interceptors" -// NOTE: To be deprecated -// import { InterceptorsPlatformDef } from "./interceptors" -// import { IOPlatformDef } from "./io" import { LimitsPlatformDef } from "./limits" import { SettingsPlatformDef } from "./settings" import { SpotlightPlatformDef } from "./spotlight" @@ -29,8 +26,6 @@ export type PlatformDef = { addedServices?: Array> auth: AuthPlatformDef analytics?: AnalyticsPlatformDef - // NOTE: To be deprecated - // io: IOPlatformDef kernelIO: KernelIO instance: InstancePlatformDef sync: { @@ -39,8 +34,6 @@ export type PlatformDef = { settings: SettingsPlatformDef history: HistoryPlatformDef } - // NOTE: To be deprecated - // interceptors: InterceptorsPlatformDef kernelInterceptors: KernelInterceptorsPlatformDef instance?: InstancePlatformDef additionalInspectors?: InspectorsPlatformDef diff --git a/packages/hoppscotch-common/src/platform/interceptors.ts b/packages/hoppscotch-common/src/platform/interceptors.ts deleted file mode 100644 index 6c2cd832..00000000 --- a/packages/hoppscotch-common/src/platform/interceptors.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Container, ServiceClassInstance } from "dioc" -import { Interceptor } from "~/services/interceptor.service" - -export type PlatformInterceptorDef = - | { type: "standalone"; interceptor: Interceptor } - | { - type: "service" - // TODO: I don't think this type is effective, we have to come up with a better impl - service: ServiceClassInstance & { - new (c: Container): Interceptor - } - } - -export type InterceptorsPlatformDef = { - default: string - interceptors: PlatformInterceptorDef[] -} diff --git a/packages/hoppscotch-common/src/platform/std/inspections/__tests__/extension.inspector.spec.ts b/packages/hoppscotch-common/src/platform/std/inspections/__tests__/extension.inspector.spec.ts deleted file mode 100644 index 3630c29f..00000000 --- a/packages/hoppscotch-common/src/platform/std/inspections/__tests__/extension.inspector.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { TestContainer } from "dioc/testing" -import { describe, expect, it, vi } from "vitest" -import { ExtensionInspectorService } from "../extension.inspector" -import { InspectionService } from "~/services/inspection" -import { getDefaultRESTRequest } from "~/helpers/rest/default" -import { ref } from "vue" -import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension" - -vi.mock("~/modules/i18n", () => ({ - __esModule: true, - getI18n: () => (x: string) => x, -})) - -describe("ExtensionInspectorService", () => { - it("registers with the inspection service upon initialization", () => { - const container = new TestContainer() - - const registerInspectorFn = vi.fn() - - container.bindMock(InspectionService, { - registerInspector: registerInspectorFn, - }) - - const urlInspector = container.bind(ExtensionInspectorService) - - expect(registerInspectorFn).toHaveBeenCalledOnce() - expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector) - }) - - describe("getInspectorFor", () => { - it("should return an inspector result when localhost is in URL and extension is not available", () => { - const container = new TestContainer() - const urlInspector = container.bind(ExtensionInspectorService) - - const req = ref({ - ...getDefaultRESTRequest(), - endpoint: "http://localhost:8000/api/data", - }) - - const result = urlInspector.getInspections(req) - - expect(result.value).toContainEqual( - expect.objectContaining({ id: "url", isApplicable: true }) - ) - }) - - it("should not return an inspector result when localhost is not in URL", () => { - const container = new TestContainer() - - container.bindMock(ExtensionInterceptorService, { - extensionStatus: ref("unknown-origin" as const), - }) - - const urlInspector = container.bind(ExtensionInspectorService) - - const req = ref({ - ...getDefaultRESTRequest(), - endpoint: "http://example.com/api/data", - }) - - const result = urlInspector.getInspections(req) - - expect(result.value).toHaveLength(0) - }) - - it("should add the correct text to the results when extension is not installed", () => { - const container = new TestContainer() - - container.bindMock(ExtensionInterceptorService, { - extensionStatus: ref("waiting" as const), - }) - - const urlInspector = container.bind(ExtensionInspectorService) - - const req = ref({ - ...getDefaultRESTRequest(), - endpoint: "http://localhost:8000/api/data", - }) - - const result = urlInspector.getInspections(req) - - expect(result.value).toHaveLength(1) - expect(result.value[0]).toMatchObject({ - text: { type: "text", text: "inspections.url.extension_not_installed" }, - }) - }) - }) -}) diff --git a/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts b/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts deleted file mode 100644 index ea9b9a61..00000000 --- a/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Service } from "dioc" -import { - InspectionService, - Inspector, - InspectorResult, -} from "~/services/inspection" -import { getI18n } from "~/modules/i18n" -import { HoppRESTRequest } from "@hoppscotch/data" -import { computed, markRaw, Ref } from "vue" -import IconAlertTriangle from "~icons/lucide/alert-triangle" -import { InterceptorService } from "~/services/interceptor.service" -import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension" - -/** - * This inspector is responsible for inspecting the URL of a request. - * It checks if the URL contains localhost and if the extension is installed. - * It also provides an action to enable the extension. - * - * NOTE: Initializing this service registers it as a inspector with the Inspection Service. - */ -export class ExtensionInspectorService extends Service implements Inspector { - public static readonly ID = "EXTENSION_INSPECTOR_SERVICE" - - private t = getI18n() - - public readonly inspectorID = "extension" - - private readonly interceptorService = this.bind(InterceptorService) - private readonly extensionService = this.bind(ExtensionInterceptorService) - - private readonly inspection = this.bind(InspectionService) - - override onServiceInit() { - this.inspection.registerInspector(this) - } - - getInspections(req: Readonly>) { - const currentExtensionStatus = this.extensionService.extensionStatus - - const isExtensionInstalled = computed( - () => currentExtensionStatus.value === "available" - ) - - const activeInterceptor = computed( - () => this.interceptorService.currentInterceptorID.value - ) - - const EXTENSION_ENABLED = computed( - () => activeInterceptor.value === "extension" - ) - - const AGENT_ENABLED = computed(() => activeInterceptor.value === "agent") - - return computed(() => { - const results: InspectorResult[] = [] - - if (!req.value) return results - - const url = req.value.endpoint - const localHostURLs = ["localhost", "127.0.0.1"] - - const isContainLocalhost = localHostURLs.some((host) => - url.includes(host) - ) - - // Prompt the user to install or enable the extension via inspector if the endpoint is `localhost`, and an interceptor other than `Agent` is active - if ( - isContainLocalhost && - !AGENT_ENABLED.value && - (!EXTENSION_ENABLED.value || !isExtensionInstalled.value) - ) { - let text - - if (!isExtensionInstalled.value) { - if (currentExtensionStatus.value === "unknown-origin") { - text = this.t("inspections.url.extension_unknown_origin") - } else { - text = this.t("inspections.url.extension_not_installed") - } - } else if (!EXTENSION_ENABLED.value) { - text = this.t("inspections.url.extention_not_enabled") - } else { - text = this.t("inspections.url.localhost") - } - - results.push({ - id: "url", - icon: markRaw(IconAlertTriangle), - text: { - type: "text", - text: text, - }, - action: { - text: this.t("inspections.url.extention_enable_action"), - apply: () => { - this.interceptorService.currentInterceptorID.value = "extension" - }, - }, - severity: 2, - isApplicable: true, - locations: { - type: "url", - }, - doc: { - text: this.t("action.learn_more"), - link: "https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension", - }, - }) - } - - return results - }) - } -} diff --git a/packages/hoppscotch-common/src/platform/std/interceptors/agent/index.ts b/packages/hoppscotch-common/src/platform/std/interceptors/agent/index.ts deleted file mode 100644 index b87f24aa..00000000 --- a/packages/hoppscotch-common/src/platform/std/interceptors/agent/index.ts +++ /dev/null @@ -1,837 +0,0 @@ -import { CookieJarService } from "~/services/cookie-jar.service" -import { - Interceptor, - InterceptorError, - InterceptorService, - RequestRunResult, -} from "~/services/interceptor.service" -import { Service } from "dioc" -import * as E from "fp-ts/Either" -import { ref, watch } from "vue" -import { z } from "zod" -import { PersistenceService } from "~/services/persistence" -import { - CACertStore, - ClientCertsStore, - ClientCertStore, - StoredClientCert, -} from "./persisted-data" -import axios, { CancelTokenSource } from "axios" -import SettingsAgentInterceptor from "~/components/settings/Agent.vue" -import AgentRootUIExtension from "~/components/interceptors/agent/RootExt.vue" -import { UIExtensionService } from "~/services/ui-extension.service" -import { x25519 } from "@noble/curves/ed25519.js" -import { base16 } from "@scure/base" -import { invokeAction } from "~/helpers/actions" -import { preProcessRequest } from "../helpers" - -type KeyValuePair = { - key: string - value: string -} - -type FormDataValue = - | { Text: string } - | { - File: { - filename: string - data: number[] - mime: string - } - } - -type FormDataEntry = { - key: string - value: FormDataValue -} - -type BodyDef = - | { Text: string } - | { URLEncoded: KeyValuePair[] } - | { FormData: FormDataEntry[] } - -type ClientCertDef = - | { - PEMCert: { - certificate_pem: number[] - key_pem: number[] - } - } - | { - PFXCert: { - certificate_pfx: number[] - password: string - } - } - -// TODO: Figure out a way to autogen this from the interceptor definition on the Rust side -export type RequestDef = { - req_id: number - - method: string - endpoint: string - - headers: KeyValuePair[] - - body: BodyDef | null - - validate_certs: boolean - root_cert_bundle_files: number[][] - client_cert: ClientCertDef | null - - proxy?: { - url: string - } -} - -type RunRequestResponse = { - status: number - status_text: string - headers: KeyValuePair[] - data: number[] - - time_start_ms: number - time_end_ms: number -} - -// HACK: To solve the AxiosRequestConfig being different between @hoppscotch/common -// and the axios present in this package -type AxiosRequestConfig = Parameters[0] - -async function processBody( - axiosReq: AxiosRequestConfig -): Promise { - if (!axiosReq.data) return null - - if (typeof axiosReq.data === "string") { - return { Text: axiosReq.data } - } - - if (axiosReq.data instanceof FormData) { - const entries: FormDataEntry[] = [] - - for (const [key, value] of axiosReq.data.entries()) { - if (typeof value === "string") { - entries.push({ - key, - value: { Text: value }, - }) - } else { - const mime = value.type !== "" ? value.type : "application/octet-stream" - - entries.push({ - key, - value: { - File: { - filename: value.name, - data: Array.from(new Uint8Array(await value.arrayBuffer())), - mime, - }, - }, - }) - } - } - - return { FormData: entries } - } - - throw new Error("Agent Process Body: Unhandled Axios Request Configuration") -} - -function getURLDomain(url: string): string | null { - try { - return new URL(url).host - } catch (_) { - return null - } -} - -function convertClientCertToDefCert( - cert: ClientCertificateEntry -): ClientCertDef { - if ("PEMCert" in cert.cert) { - return { - PEMCert: { - certificate_pem: Array.from(cert.cert.PEMCert.certificate_pem), - key_pem: Array.from(cert.cert.PEMCert.key_pem), - }, - } - } - return { - PFXCert: { - certificate_pfx: Array.from(cert.cert.PFXCert.certificate_pfx), - password: cert.cert.PFXCert.password, - }, - } -} - -async function convertToRequestDef( - axiosReq: AxiosRequestConfig, - reqID: number, - caCertificates: CACertificateEntry[], - clientCertificates: Map, - validateCerts: boolean, - proxyInfo: RequestDef["proxy"] -): Promise { - const clientCertDomain = getURLDomain(axiosReq.url!) - - const clientCert = clientCertDomain - ? clientCertificates.get(clientCertDomain) - : null - - const urlObj = new URL(axiosReq.url ?? "") - - // If there are parameters in axiosReq.params, add them to the URL. - if (axiosReq.params) { - const params = new URLSearchParams(urlObj.search) // Taking in existing params if are any. - - Object.entries(axiosReq.params as Record).forEach( - ([key, value]) => { - params.append(key, value) - } - ) - - urlObj.search = params.toString() // Now put back all the params in the URL. - } - - return { - req_id: reqID, - method: axiosReq.method ?? "GET", - endpoint: urlObj.toString(), // This is the updated URL with parms. - headers: Object.entries(axiosReq.headers ?? {}) - .filter( - ([key, value]) => - !( - key.toLowerCase() === "content-type" && - value.toLowerCase() === "multipart/form-data" - ) - ) // Removing header, because this header will be set by agent. - .map(([key, value]): KeyValuePair => ({ key, value })), - - // NOTE: Injected parameters are already part of the URL - - body: await processBody(axiosReq), - root_cert_bundle_files: caCertificates.map((cert) => - Array.from(cert.certificate) - ), - validate_certs: validateCerts, - client_cert: clientCert ? convertClientCertToDefCert(clientCert) : null, - proxy: proxyInfo, - } -} - -export const CACertificateEntry = z.object({ - filename: z.string().min(1), - enabled: z.boolean(), - certificate: z.instanceof(Uint8Array), -}) - -export type CACertificateEntry = z.infer - -export const ClientCertificateEntry = z.object({ - enabled: z.boolean(), - domain: z.string().trim().min(1), - cert: z.union([ - z.object({ - PEMCert: z.object({ - certificate_filename: z.string().min(1), - certificate_pem: z.instanceof(Uint8Array), - - key_filename: z.string().min(1), - key_pem: z.instanceof(Uint8Array), - }), - }), - z.object({ - PFXCert: z.object({ - certificate_filename: z.string().min(1), - certificate_pfx: z.instanceof(Uint8Array), - - password: z.string(), - }), - }), - ]), -}) - -export type ClientCertificateEntry = z.infer - -const CA_STORE_PERSIST_KEY = "agent_interceptor_ca_store" -const CLIENT_CERTS_PERSIST_KEY = "agent_interceptor_client_certs_store" -const VALIDATE_SSL_KEY = "agent_interceptor_validate_ssl" -const AUTH_KEY_PERSIST_KEY = "agent_interceptor_auth_key" -const SHARED_SECRET_PERSIST_KEY = "agent_interceptor_shared_secret" -const PROXY_INFO_PERSIST_KEY = "agent_interceptor_proxy_info" - -export class AgentInterceptorService extends Service implements Interceptor { - public static readonly ID = "AGENT_INTERCEPTOR_SERVICE" - - public interceptorID = "agent" - - // TODO: Better User facing name - public name = () => "Agent" - - public selectable = { type: "selectable" as const } - - public supportsDigestAuth = true - public supportsCookies = true - public supportsBinaryContentType = false - - private interceptorService = this.bind(InterceptorService) - private cookieJarService = this.bind(CookieJarService) - private persistenceService = this.bind(PersistenceService) - private uiExtensionService = this.bind(UIExtensionService) - - public isAgentRunning = ref(false) - - private reqIDTicker = 0 - private cancelTokens: Map = new Map() - - public settingsPageEntry = { - entryTitle: () => "Agent", // TODO: i18n this - component: SettingsAgentInterceptor, - } - - public caCertificates = ref([]) - - public clientCertificates = ref>( - new Map() - ) - public validateCerts = ref(true) - - public showRegistrationModal = ref(false) - public authKey = ref(null) - public sharedSecretB16 = ref(null) - private registrationOTP = ref(null) - - public proxyInfo = ref(undefined) - - override onServiceInit() { - // Register the Root UI Extension - this.uiExtensionService.addRootUIExtension(AgentRootUIExtension) - - const persistedAuthKey = - this.persistenceService.getLocalConfig(AUTH_KEY_PERSIST_KEY) - if (persistedAuthKey) { - this.authKey.value = persistedAuthKey - } - - const sharedSecret = this.persistenceService.getLocalConfig( - SHARED_SECRET_PERSIST_KEY - ) - if (sharedSecret) { - this.sharedSecretB16.value = sharedSecret - } - - const persistedProxyInfo = this.persistenceService.getLocalConfig( - PROXY_INFO_PERSIST_KEY - ) - if (persistedProxyInfo && persistedProxyInfo !== "null") { - try { - const proxyInfo = JSON.parse(persistedProxyInfo) - this.proxyInfo.value = proxyInfo - } catch (_e) {} - } - - // Load SSL Validation - const persistedValidateSSL: unknown = JSON.parse( - this.persistenceService.getLocalConfig(VALIDATE_SSL_KEY) ?? "null" - ) - - if (typeof persistedValidateSSL === "boolean") { - this.validateCerts.value = persistedValidateSSL - } - - watch(this.validateCerts, () => { - this.persistenceService.setLocalConfig( - VALIDATE_SSL_KEY, - JSON.stringify(this.validateCerts.value) - ) - }) - - // Load and setup writes for CA Store - const persistedCAStoreData = JSON.parse( - this.persistenceService.getLocalConfig(CA_STORE_PERSIST_KEY) ?? "null" - ) - - const caStoreDataParseResult = CACertStore.safeParse(persistedCAStoreData) - - if (caStoreDataParseResult.type === "ok") { - this.caCertificates.value = caStoreDataParseResult.value.certs.map( - (entry) => ({ - ...entry, - certificate: new Uint8Array(entry.certificate), - }) - ) - } - - watch(this.caCertificates, (certs) => { - const storableValue: CACertStore = { - v: 1, - certs: certs.map((el) => ({ - ...el, - certificate: Array.from(el.certificate), - })), - } - - this.persistenceService.setLocalConfig( - CA_STORE_PERSIST_KEY, - JSON.stringify(storableValue) - ) - }) - - // Load and setup writes for Client Certs Store - const persistedClientCertStoreData = JSON.parse( - this.persistenceService.getLocalConfig(CLIENT_CERTS_PERSIST_KEY) ?? "null" - ) - - const clientCertStoreDataParseResult = ClientCertsStore.safeParse( - persistedClientCertStoreData - ) - - if (clientCertStoreDataParseResult.type === "ok") { - this.clientCertificates.value = new Map( - Object.entries(clientCertStoreDataParseResult.value.clientCerts).map( - ([domain, cert]) => { - if ("PFXCert" in cert.cert) { - const newCert = { - ...cert, - cert: { - PFXCert: { - certificate_pfx: new Uint8Array( - cert.cert.PFXCert.certificate_pfx - ), - certificate_filename: - cert.cert.PFXCert.certificate_filename, - - password: cert.cert.PFXCert.password, - }, - }, - } - - return [domain, newCert] - } - const newCert = { - ...cert, - cert: { - PEMCert: { - certificate_pem: new Uint8Array( - cert.cert.PEMCert.certificate_pem - ), - certificate_filename: cert.cert.PEMCert.certificate_filename, - - key_pem: new Uint8Array(cert.cert.PEMCert.key_pem), - key_filename: cert.cert.PEMCert.key_filename, - }, - }, - } - - return [domain, newCert] - } - ) - ) - } - - watch(this.clientCertificates, (certs) => { - const storableValue: ClientCertStore = { - v: 1, - clientCerts: Object.fromEntries( - Array.from(certs.entries()).map(([domain, cert]) => { - if ("PFXCert" in cert.cert) { - const newCert = { - ...cert, - cert: { - PFXCert: { - certificate_pfx: Array.from( - cert.cert.PFXCert.certificate_pfx - ), - certificate_filename: - cert.cert.PFXCert.certificate_filename, - - password: cert.cert.PFXCert.password, - }, - }, - } - - return [domain, newCert] - } - const newCert = { - ...cert, - cert: { - PEMCert: { - certificate_pem: Array.from( - cert.cert.PEMCert.certificate_pem - ), - certificate_filename: cert.cert.PEMCert.certificate_filename, - - key_pem: Array.from(cert.cert.PEMCert.key_pem), - key_filename: cert.cert.PEMCert.key_filename, - }, - }, - } - - return [domain, newCert] - }) - ), - } - - this.persistenceService.setLocalConfig( - CLIENT_CERTS_PERSIST_KEY, - JSON.stringify(storableValue) - ) - }) - - watch(this.authKey, (newAuthKey) => { - if (newAuthKey) { - this.persistenceService.setLocalConfig(AUTH_KEY_PERSIST_KEY, newAuthKey) - } else { - this.persistenceService.removeLocalConfig(AUTH_KEY_PERSIST_KEY) - } - }) - - watch(this.proxyInfo, (newProxyInfo) => { - this.persistenceService.setLocalConfig( - PROXY_INFO_PERSIST_KEY, - JSON.stringify(newProxyInfo) ?? "null" - ) - }) - - // Show registration UI if there is no auth key present - watch( - [this.interceptorService.currentInterceptor, this.authKey], - ([currentInterceptor, authKey]) => { - if ( - currentInterceptor?.interceptorID === this.interceptorID && - authKey === null - ) { - this.showRegistrationModal.value = true - } - }, - { - immediate: true, - } - ) - - // Verify if the agent registration still holds, else revoke the registration - if (this.authKey.value) { - ;(async () => { - try { - const nonce = window.crypto.getRandomValues(new Uint8Array(12)) - const nonceB16 = base16.encode(nonce).toLowerCase() - - const response = await axios.get( - "http://localhost:9119/registered-handshake", - { - headers: { - Authorization: `Bearer ${this.authKey.value}`, - "X-Hopp-Nonce": nonceB16, - }, - responseType: "arraybuffer", - } - ) - - const responseNonceB16: string = response.headers["x-hopp-nonce"] - const encryptedResponseBytes = response.data - - const parsedData = await this.getDecryptedResponse( - responseNonceB16, - encryptedResponseBytes - ) - - // This should decrypt directly into `true` else registration failed - if (parsedData !== true) { - throw "handshake-mismatch" - } - } catch (e) { - if (e === "handshake-mismatch") { - this.sharedSecretB16.value = null - this.authKey.value = null - } else if (axios.isAxiosError(e) && e.status === 401) { - this.sharedSecretB16.value = null - this.authKey.value = null - } - } - })() - } - } - - public async checkAgentStatus(): Promise { - try { - await this.performHandshake() - this.isAgentRunning.value = true - } catch (_error) { - this.isAgentRunning.value = false - } - } - - public isAuthKeyPresent(): boolean { - return this.authKey.value !== null - } - - private generateOTP(): string { - // This generates a 6-digit numeric OTP - return Math.floor(100000 + Math.random() * 900000).toString() - } - - public async performHandshake(): Promise { - const handshakeResponse = await axios.get("http://localhost:9119/handshake") - if ( - handshakeResponse.data.status !== "success" && - handshakeResponse.data.__hoppscotch__agent__ === true - ) { - throw new Error("Handshake failed") - } - } - - public async initiateRegistration() { - try { - // Generate OTP and send registration request - this.registrationOTP.value = this.generateOTP() - - const registrationResponse = await axios.post( - "http://localhost:9119/receive-registration", - { - registration: this.registrationOTP.value, - } - ) - - if ( - registrationResponse.data.message !== "Registration received and stored" - ) { - throw new Error("Registration failed") - } - - // Registration successful, modal will handle showing the OTP input - } catch (error) { - console.error("Registration initiation failed:", error) - throw error // Re-throw to let the modal handle the error - } - } - - public async verifyRegistration(userEnteredOTP: string) { - try { - const myPrivateKey = x25519.utils.randomPrivateKey() - const myPublicKey = x25519.getPublicKey(myPrivateKey) - - const myPublicKeyB16 = base16.encode(myPublicKey).toLowerCase() - - const verificationResponse = await axios.post( - "http://localhost:9119/verify-registration", - { - registration: userEnteredOTP, - client_public_key_b16: myPublicKeyB16, - } - ) - - const newAuthKey = verificationResponse.data.auth_key - const agentPublicKeyB16: string = - verificationResponse.data.agent_public_key_b16 - - const agentPublicKey = base16.decode(agentPublicKeyB16.toUpperCase()) - - const sharedSecret = x25519.getSharedSecret(myPrivateKey, agentPublicKey) - const sharedSecretB16 = base16.encode(sharedSecret).toLowerCase() - - if (typeof newAuthKey === "string") { - this.authKey.value = newAuthKey - this.sharedSecretB16.value = sharedSecretB16 - this.persistenceService.setLocalConfig(AUTH_KEY_PERSIST_KEY, newAuthKey) - this.persistenceService.setLocalConfig( - SHARED_SECRET_PERSIST_KEY, - sharedSecretB16 - ) - } else { - throw new Error("Invalid auth key received") - } - - this.showRegistrationModal.value = false - this.registrationOTP.value = null - } catch (error) { - console.error("Verification failed:", error) - throw new Error("Verification failed") - } - } - - private async getEncryptedRequestDef( - def: RequestDef - ): Promise<[string, ArrayBuffer]> { - const defJSON = JSON.stringify(def) - const defJSONBytes = new TextEncoder().encode(defJSON) - - const nonce = window.crypto.getRandomValues(new Uint8Array(12)) - const nonceB16 = base16.encode(nonce).toLowerCase() - - const sharedSecretKeyBytes = base16.decode( - this.sharedSecretB16.value!.toUpperCase() - ) - - const sharedSecretKey = await window.crypto.subtle.importKey( - "raw", - sharedSecretKeyBytes, - "AES-GCM", - true, - ["encrypt", "decrypt"] - ) - - const encryptedDef = await window.crypto.subtle.encrypt( - { name: "AES-GCM", iv: nonce }, - sharedSecretKey, - defJSONBytes - ) - - return [nonceB16, encryptedDef] - } - - private async getDecryptedResponse( - nonceB16: string, - responseData: ArrayBuffer - ) { - const sharedSecretKeyBytes = base16.decode( - this.sharedSecretB16.value!.toUpperCase() - ) - - const sharedSecretKey = await window.crypto.subtle.importKey( - "raw", - sharedSecretKeyBytes, - "AES-GCM", - true, - ["encrypt", "decrypt"] - ) - - const nonce = base16.decode(nonceB16.toUpperCase()) - - const plainTextDefBytes = await window.crypto.subtle.decrypt( - { name: "AES-GCM", iv: nonce }, - sharedSecretKey, - responseData - ) - - const plainText = new TextDecoder().decode(plainTextDefBytes) - - return JSON.parse(plainText) as T - } - - public runRequest( - req: AxiosRequestConfig - ): RequestRunResult { - // TODO: Check if auth key is defined ? - - const processedReq = preProcessRequest(req) - - const relevantCookies = this.cookieJarService.getCookiesForURL( - new URL(processedReq.url!) - ) - - if (relevantCookies.length > 0) { - processedReq.headers!["Cookie"] = relevantCookies - .map((cookie) => `${cookie.name!}=${cookie.value!}`) - .join(";") - } - - const reqID = this.reqIDTicker++ - - const cancelTokenSource = axios.CancelToken.source() - this.cancelTokens.set(reqID, cancelTokenSource) - - return { - cancel: () => { - const cancelTokenSource = this.cancelTokens.get(reqID) - if (cancelTokenSource) { - cancelTokenSource.cancel("Request cancelled") - this.cancelTokens.delete(reqID) - axios - .post( - `http://localhost:9119/cancel-request/${reqID}`, - {}, - { - headers: { - Authorization: `Bearer ${this.authKey.value}`, - }, - } - ) - .catch((error) => console.error("Error cancelling request:", error)) - } - }, - response: (async () => { - await this.checkAgentStatus() - - if (!this.isAgentRunning.value || !this.authKey.value) { - invokeAction("agent.open-registration-modal") - - return E.left({ - humanMessage: { - heading: (t) => t("error.network_fail"), - description: (t) => t("helpers.network_fail"), - }, - }) - } - - const requestDef = await convertToRequestDef( - processedReq, - reqID, - this.caCertificates.value, - this.clientCertificates.value, - this.validateCerts.value, - this.proxyInfo.value - ) - - const [nonceB16, encryptedDef] = - await this.getEncryptedRequestDef(requestDef) - - try { - const http_response = await axios.post( - "http://localhost:9119/request", - encryptedDef, - { - headers: { - Authorization: `Bearer ${this.authKey.value}`, - "X-Hopp-Nonce": nonceB16, - "Content-Type": "application/octet-stream", - }, - cancelToken: cancelTokenSource.token, - responseType: "arraybuffer", - } - ) - - const responseNonceB16: string = http_response.headers["x-hopp-nonce"] - const encryptedResponseBytes = http_response.data - - const response = await this.getDecryptedResponse( - responseNonceB16, - encryptedResponseBytes - ) - - // TODO: Run it against a Zod Schema validation - - return E.right({ - headers: Object.fromEntries( - response.headers.map(({ key, value }) => [key, value]) - ), - status: response.status, - statusText: response.status_text, - data: new Uint8Array(response.data).buffer, - config: { - timeData: { - startTime: response.time_start_ms, - endTime: response.time_end_ms, - }, - }, - additional: { - multiHeaders: response.headers, - }, - }) - } catch (e) { - if (typeof e === "object" && (e as any)["RequestCancelled"]) { - return E.left("cancellation" as const) - } - - // TODO: More in-depth error messages - return E.left({ - humanMessage: { - heading: (t) => t("error.network_fail"), - description: (t) => t("helpers.network_fail"), - }, - }) - } - })(), - } - } -} diff --git a/packages/hoppscotch-common/src/platform/std/interceptors/agent/persisted-data.ts b/packages/hoppscotch-common/src/platform/std/interceptors/agent/persisted-data.ts deleted file mode 100644 index 166c3108..00000000 --- a/packages/hoppscotch-common/src/platform/std/interceptors/agent/persisted-data.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { z } from "zod" -import { defineVersion, createVersionedEntity, InferredEntity } from "verzod" - -const Uint8 = z.number().int().gte(0).lte(255) - -export const StoredCACert = z.object({ - filename: z.string().min(1), - enabled: z.boolean(), - certificate: z.array(Uint8), -}) - -const caCertStore_v1 = defineVersion({ - initial: true, - schema: z.object({ - v: z.literal(1), - certs: z.array(StoredCACert), - }), -}) - -export const CACertStore = createVersionedEntity({ - latestVersion: 1, - versionMap: { - 1: caCertStore_v1, - }, - getVersion(data) { - const result = caCertStore_v1.schema.safeParse(data) - - return result.success ? result.data.v : null - }, -}) - -export type CACertStore = InferredEntity - -export const StoredClientCert = z.object({ - enabled: z.boolean(), - domain: z.string().trim().min(1), - cert: z.union([ - z.object({ - PEMCert: z.object({ - certificate_filename: z.string().min(1), - certificate_pem: z.array(Uint8), - - key_filename: z.string().min(1), - key_pem: z.array(Uint8), - }), - }), - z.object({ - PFXCert: z.object({ - certificate_filename: z.string().min(1), - certificate_pfx: z.array(Uint8), - - password: z.string(), - }), - }), - ]), -}) - -export type StoredClientCert = z.infer - -const clientCertsStore_v1 = defineVersion({ - initial: true, - schema: z.object({ - v: z.literal(1), - clientCerts: z.record(StoredClientCert), - }), -}) - -export const ClientCertsStore = createVersionedEntity({ - latestVersion: 1, - versionMap: { - 1: clientCertsStore_v1, - }, - getVersion(data) { - const result = clientCertsStore_v1.schema.safeParse(data) - - return result.success ? result.data.v : null - }, -}) - -export type ClientCertStore = InferredEntity diff --git a/packages/hoppscotch-common/src/platform/std/interceptors/extension.ts b/packages/hoppscotch-common/src/platform/std/interceptors/extension.ts deleted file mode 100644 index de5efb12..00000000 --- a/packages/hoppscotch-common/src/platform/std/interceptors/extension.ts +++ /dev/null @@ -1,240 +0,0 @@ -import * as E from "fp-ts/Either" -import { AxiosRequestConfig } from "axios" -import { Service } from "dioc" -import { getI18n } from "~/modules/i18n" -import { - Interceptor, - InterceptorError, - RequestRunResult, -} from "~/services/interceptor.service" -import { computed, readonly, ref } from "vue" -import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent" -import SettingsExtension from "~/components/settings/Extension.vue" -import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue" -import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue" -import { until } from "@vueuse/core" -import { preProcessRequest } from "./helpers" - -export const defineSubscribableObject = (obj: T) => { - const proxyObject = { - ...obj, - _subscribers: {} as { - // eslint-disable-next-line no-unused-vars - [key in keyof T]?: ((...args: any[]) => any)[] - }, - subscribe(prop: keyof T, func: (...args: any[]) => any): void { - if (Array.isArray(this._subscribers[prop])) { - this._subscribers[prop]?.push(func) - } else { - this._subscribers[prop] = [func] - } - }, - } - - type SubscribableProxyObject = typeof proxyObject - - return new Proxy(proxyObject, { - set(obj, prop, newVal) { - obj[prop as keyof SubscribableProxyObject] = newVal - - const currentSubscribers = obj._subscribers[prop as keyof T] - - if (Array.isArray(currentSubscribers)) { - for (const subscriber of currentSubscribers) { - subscriber(newVal) - } - } - - return true - }, - }) -} - -// TODO: Rework this to deal with individual requests rather than cancel all -export const cancelRunningExtensionRequest = () => { - window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRequest() -} - -export type ExtensionStatus = "available" | "unknown-origin" | "waiting" - -/** - * This service is responsible for defining the extension interceptor. - */ -export class ExtensionInterceptorService - extends Service - implements Interceptor -{ - public static readonly ID = "EXTENSION_INTERCEPTOR_SERVICE" - - private _extensionStatus = ref("waiting") - - /** - * The status of the extension, whether it's available, or not. - */ - public extensionStatus = readonly(this._extensionStatus) - - /** - * The version of the extension, if available. - */ - public extensionVersion = computed(() => { - if (this.extensionStatus.value === "available") { - return window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() - } - return null - }) - - /** - * Whether the extension is installed in Chrome or not. - */ - public chromeExtensionInstalled = computed( - () => this.extensionStatus.value === "available" && browserIsChrome() - ) - - /** - * Whether the extension is installed in Firefox or not. - */ - public firefoxExtensionInstalled = computed( - () => this.extensionStatus.value === "available" && browserIsFirefox() - ) - - public interceptorID = "extension" - - public settingsPageEntry: Interceptor["settingsPageEntry"] = { - entryTitle: (t) => t("settings.extensions"), - component: SettingsExtension, - } - - public selectorSubtitle = InterceptorsExtensionSubtitle - - public selectable = { type: "selectable" as const } - - override onServiceInit() { - this.listenForExtensionStatus() - } - - private listenForExtensionStatus() { - const extensionPollIntervalId = ref>() - - if (window.__HOPP_EXTENSION_STATUS_PROXY__) { - this._extensionStatus.value = - window.__HOPP_EXTENSION_STATUS_PROXY__.status - - window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe( - "status", - (status: ExtensionStatus) => { - this._extensionStatus.value = status - } - ) - } else { - const statusProxy = defineSubscribableObject({ - status: "waiting" as ExtensionStatus, - }) - - window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy - statusProxy.subscribe( - "status", - (status: ExtensionStatus) => (this._extensionStatus.value = status) - ) - - /** - * Keeping identifying extension backward compatible - * We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately, - * then we use a poll to find the version, this will get the version for 0.24 and any other version - * of the extension, but will have a slight lag. - * 0.24 users will get the benefits of 0.24, while the extension won't break for the old users - */ - extensionPollIntervalId.value = setInterval(() => { - if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { - if (extensionPollIntervalId.value) - clearInterval(extensionPollIntervalId.value) - - const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() - - // When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually - if ( - version.major === 0 && - version.minor <= 23 && - window.__HOPP_EXTENSION_STATUS_PROXY__ - ) { - window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available" - } - } - }, 2000) - } - } - - public name(t: ReturnType) { - return computed(() => { - const version = window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() - - if (this.extensionStatus.value === "available" && version) { - const { major, minor } = version - return `${t("settings.extensions")}: v${major}.${minor}` - } - return `${t("settings.extensions")}: ${t( - "settings.extension_ver_not_reported" - )}` - }) - } - - private async runRequestOnExtension( - req: AxiosRequestConfig - ): RequestRunResult["response"] { - // wait for the extension to resolve - await until(this.extensionStatus).toMatch( - (status) => status !== "waiting", - { - timeout: 1000, - } - ) - - const extensionHook = window.__POSTWOMAN_EXTENSION_HOOK__ - if (!extensionHook) { - return E.left({ - // TODO: i18n this - humanMessage: { - heading: () => "Extension not found", - description: () => "Heading not found", - }, - error: "NO_PW_EXT_HOOK", - component: InterceptorsErrorPlaceholder, - }) - } - - try { - const result = await extensionHook.sendRequest({ - ...req, - headers: req.headers ?? {}, - wantsBinary: true, - }) - - return E.right(result) - } catch (e) { - console.error(e) - // TODO: improve type checking - if ((e as any).response) { - return E.right((e as any).response) - } - - return E.left({ - // TODO: i18n this - humanMessage: { - heading: () => "Extension error", - description: () => "Failed running request on extension", - }, - error: e, - }) - } - } - - public runRequest( - request: AxiosRequestConfig - ): RequestRunResult { - const processedReq = preProcessRequest(request) - - return { - cancel: cancelRunningExtensionRequest, - response: this.runRequestOnExtension(processedReq), - } - } -} diff --git a/packages/hoppscotch-common/src/platform/std/interceptors/helpers.ts b/packages/hoppscotch-common/src/platform/std/interceptors/helpers.ts deleted file mode 100644 index 64508e9a..00000000 --- a/packages/hoppscotch-common/src/platform/std/interceptors/helpers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AxiosRequestConfig } from "axios" -import { cloneDeep } from "lodash-es" -import { useSetting } from "~/composables/settings" - -// Helper function to check if a string is already encoded -const isEncoded = (value: string) => { - try { - return value !== decodeURIComponent(value) - } catch (_e) { - return false // in case of malformed URI sequence - } -} - -export const preProcessRequest = ( - req: AxiosRequestConfig -): AxiosRequestConfig => { - const reqClone = cloneDeep(req) - const encodeMode = useSetting("ENCODE_MODE") - - // If the parameters are URLSearchParams, inject them to URL instead - // This prevents issues of marshalling the URLSearchParams to the proxy - if (reqClone.params instanceof URLSearchParams) { - try { - const url = new URL(reqClone.url ?? "") - - for (const [key, value] of reqClone.params.entries()) { - let finalValue = value - if ( - encodeMode.value === "enable" || - (encodeMode.value === "auto" && - /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(value)) - ) { - // Check if the value is already encoded (e.g., contains % symbols) - if (!isEncoded(value)) { - finalValue = encodeURIComponent(value) - } - } - - // Set the parameter with the final value - url.searchParams.append(key, finalValue) - } - - // decode the URL to prevent double encoding - reqClone.url = decodeURIComponent(url.toString()) - } catch (_e) { - // making this a non-empty block, so we can make the linter happy. - // we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :) - } - - reqClone.params = {} - } - - return reqClone -} diff --git a/packages/hoppscotch-common/src/platform/std/interceptors/proxy.ts b/packages/hoppscotch-common/src/platform/std/interceptors/proxy.ts deleted file mode 100644 index 75ec4ee3..00000000 --- a/packages/hoppscotch-common/src/platform/std/interceptors/proxy.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Interceptor, RequestRunResult } from "~/services/interceptor.service" -import axios, { AxiosRequestConfig, CancelToken } from "axios" -import * as E from "fp-ts/Either" -import { preProcessRequest } from "./helpers" -import { v4 } from "uuid" -import { settingsStore } from "~/newstore/settings" -import { decodeB64StringToArrayBuffer } from "~/helpers/utils/b64" -import SettingsProxy from "~/components/settings/Proxy.vue" -import { getDefaultProxyUrl } from "~/helpers/proxyUrl" - -type ProxyHeaders = { - "multipart-part-key"?: string -} - -type ProxyPayloadType = - | FormData - | (AxiosRequestConfig & { wantsBinary: true; accessToken: string }) - -const getProxyPayload = ( - req: AxiosRequestConfig, - multipartKey: string | null -) => { - let payload: ProxyPayloadType = { - ...req, - wantsBinary: true, - accessToken: import.meta.env.VITE_PROXYSCOTCH_ACCESS_TOKEN ?? "", - } - - if (payload.data instanceof FormData) { - const formData = payload.data - payload.data = "" - formData.append(multipartKey!, JSON.stringify(payload)) - payload = formData - } - - return payload -} - -async function runRequest( - req: AxiosRequestConfig, - cancelToken: CancelToken -): RequestRunResult["response"] { - const defaultProxyURL = await getDefaultProxyUrl() - - const multipartKey = - req.data instanceof FormData ? `proxyRequestData-${v4()}` : null - - const headers = - req.data instanceof FormData - ? { - "multipart-part-key": multipartKey, - } - : {} - - const payload = getProxyPayload(req, multipartKey) - - try { - // TODO: Validation for the proxy result - const { data } = await axios.post( - settingsStore.value.PROXY_URL ?? defaultProxyURL, - payload, - { - headers, - cancelToken, - } - ) - - if (!data.success) { - return E.left({ - humanMessage: { - heading: (t) => t("error.network_fail"), - description: (t) => data.data?.message ?? t("error.proxy_error"), - }, - }) - } - - if (data.isBinary) { - data.data = decodeB64StringToArrayBuffer(data.data) - } - - return E.right(data) - } catch (e) { - if (axios.isCancel(e)) { - return E.left("cancellation") - } - return E.left({ - humanMessage: { - heading: (t) => t("error.network_fail"), - description: (t) => t("helpers.network_fail"), - }, - error: e, - }) - } -} - -export const proxyInterceptor: Interceptor = { - interceptorID: "proxy", - name: (t) => t("settings.proxy"), - selectable: { type: "selectable" }, - supportsBinaryContentType: false, - settingsPageEntry: { - entryTitle: (t) => t("settings.proxy"), - component: SettingsProxy, - }, - runRequest(req) { - const cancelToken = axios.CancelToken.source() - - const processedReq = preProcessRequest(req) - - const promise = runRequest(processedReq, cancelToken.token) - - return { - cancel: () => cancelToken.cancel(), - response: promise, - } - }, -} diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts index 42f05bc3..b466e633 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts @@ -23,7 +23,7 @@ import { import { KernelInterceptorAgentStore } from "./store" import SettingsAgent from "~/components/settings/Agent.vue" import SettingsAgentSubtitle from "~/components/settings/AgentSubtitle.vue" -import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue" +import InterceptorsErrorPlaceholder from "~/components/settings/InterceptorErrorPlaceholder.vue" import { CookieJarService } from "~/services/cookie-jar.service" export class AgentKernelInterceptorService diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/index.ts index cdd81a9e..4817ab7b 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/index.ts @@ -14,7 +14,7 @@ import type { import { getI18n } from "~/modules/i18n" import { preProcessRelayRequest } from "~/helpers/functional/process-request" -import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue" +import InterceptorsErrorPlaceholder from "~/components/settings/InterceptorErrorPlaceholder.vue" export class BrowserKernelInterceptorService extends Service diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts index 428ce9b9..8adb2da6 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts @@ -20,7 +20,7 @@ import type { KernelInterceptorError, } from "~/services/kernel-interceptor.service" import { CookieJarService } from "~/services/cookie-jar.service" -import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue" +import InterceptorsErrorPlaceholder from "~/components/settings/InterceptorErrorPlaceholder.vue" import SettingsNative from "~/components/settings/Native.vue" import { KernelInterceptorNativeStore } from "./store" diff --git a/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts b/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts deleted file mode 100644 index cb5b8ea8..00000000 --- a/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { describe, expect, it, vi } from "vitest" -import { Interceptor, InterceptorService } from "../interceptor.service" -import { TestContainer } from "dioc/testing" - -describe("InterceptorService", () => { - it("initaly there are no interceptors defined", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - expect(service.availableInterceptors.value).toEqual([]) - }) - - it("currentInterceptorID should be null if no interceptors are defined", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - expect(service.currentInterceptorID.value).toBeNull() - }) - - it("currentInterceptorID should be set if there is an interceptor defined", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - service.registerInterceptor({ - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: () => { - throw new Error("Not implemented") - }, - }) - - expect(service.currentInterceptorID.value).toEqual("test") - }) - - it("currentInterceptorID cannot be set to null if there are interceptors defined", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - service.registerInterceptor({ - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: () => { - throw new Error("Not implemented") - }, - }) - - service.currentInterceptorID.value = null - expect(service.currentInterceptorID.value).not.toBeNull() - }) - - it("currentInterceptorID cannot be set to an unknown interceptor ID", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - service.registerInterceptor({ - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: () => { - throw new Error("Not implemented") - }, - }) - - service.currentInterceptorID.value = "unknown" - expect(service.currentInterceptorID.value).not.toEqual("unknown") - }) - - it("currentInterceptor points to the instance of the currently selected interceptor", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - const interceptor = { - interceptorID: "test", - name: () => "test interceptor", - selectable: { type: "selectable" as const }, - runRequest: () => { - throw new Error("not implemented") - }, - } - - service.registerInterceptor(interceptor) - service.currentInterceptorID.value = "test" - - expect(service.currentInterceptor.value).toBe(interceptor) - }) - - it("currentInterceptor updates when the currentInterceptorID changes", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - const interceptor = { - interceptorID: "test", - name: () => "test interceptor", - selectable: { type: "selectable" as const }, - runRequest: () => { - throw new Error("not implemented") - }, - } - - const interceptor_2 = { - interceptorID: "test2", - name: () => "test interceptor", - selectable: { type: "selectable" as const }, - runRequest: () => { - throw new Error("not implemented") - }, - } - - service.registerInterceptor(interceptor) - service.registerInterceptor(interceptor_2) - - service.currentInterceptorID.value = "test" - - expect(service.currentInterceptor.value).toBe(interceptor) - - service.currentInterceptorID.value = "test2" - expect(service.currentInterceptor.value).not.toBe(interceptor) - expect(service.currentInterceptor.value).toBe(interceptor_2) - }) - - describe("registerInterceptor", () => { - it("should register the interceptor", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - const interceptor: Interceptor = { - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: () => { - throw new Error("Not implemented") - }, - } - - service.registerInterceptor(interceptor) - - expect(service.availableInterceptors.value).toEqual([interceptor]) - }) - - it("should set the current interceptor ID to non-null after the initial registration", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - const interceptor: Interceptor = { - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: () => { - throw new Error("Not implemented") - }, - } - - service.registerInterceptor(interceptor) - - expect(service.currentInterceptorID.value).not.toBeNull() - }) - }) - - describe("runRequest", () => { - it("should throw an error if no interceptor is selected", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - expect(() => service.runRequest({})).toThrowError() - }) - - it("asks the current interceptor to run the request", () => { - const container = new TestContainer() - - const service = container.bind(InterceptorService) - - const interceptor: Interceptor = { - interceptorID: "test", - name: () => "Test Interceptor", - selectable: { type: "selectable" }, - runRequest: vi.fn(), - } - - service.registerInterceptor(interceptor) - - service.runRequest({}) - - expect(interceptor.runRequest).toHaveBeenCalled() - }) - }) -}) diff --git a/packages/hoppscotch-common/src/services/interceptor.service.ts b/packages/hoppscotch-common/src/services/interceptor.service.ts deleted file mode 100644 index 28f32988..00000000 --- a/packages/hoppscotch-common/src/services/interceptor.service.ts +++ /dev/null @@ -1,274 +0,0 @@ -import * as E from "fp-ts/Either" -import { Service } from "dioc" -import { refWithControl } from "@vueuse/core" -import { AxiosRequestConfig, AxiosResponse } from "axios" -import type { getI18n } from "~/modules/i18n" -import { throwError } from "~/helpers/functional/error" -import { - Component, - MaybeRef, - Ref, - computed, - reactive, - watch, - unref, - markRaw, -} from "vue" - -/** - * Defines the response data from an interceptor request run. - */ -export type NetworkResponse = AxiosResponse & { - config?: { - timeData?: { - startTime: number - endTime: number - } - } - /** - * Optional additional fields with special optional metadata that can be used - */ - additional?: { - /** - * By the HTTP spec, we can have multiple headers with the same name, but - * this is not accessible in the AxiosResponse type as the headers there are Record - * (and hence cannot have secondary values). - * - * If this value is present, headers can be read from here which will have the data. - */ - multiHeaders?: Array<{ - key: string - value: string - }> - } -} - -/** - * Defines the errors that can occur during interceptor request run. - */ -export type InterceptorError = - | "cancellation" - | { - humanMessage: { - heading: (t: ReturnType) => string - description: (t: ReturnType) => string - } - error?: unknown - component?: Component - } - -/** - * Defines the result of an interceptor request run. - */ -export type RequestRunResult = - { - /** - * Cancels the interceptor request run. - */ - cancel: () => void - - /** - * Promise that resolves when the interceptor request run is finished. - */ - response: Promise> - } - -/** - * Defines whether an interceptor is selectable or not - */ -export type InterceptorSelectableStatus = - | { type: "selectable" } - | { - type: "unselectable" - reason: - | { - type: "text" - text: (t: ReturnType) => string - action?: { - text: (t: ReturnType) => string - onActionClick: () => void - } - } - | { - type: "custom" - component: Component - props: CustomComponentProps - } - } - -/** - * An interceptor is an object that defines how to run a Hoppscotch request. - */ -export type Interceptor = { - /** - * The ID of the interceptor. This should be unique across all registered interceptors. - */ - interceptorID: string - - /** - * The function that returns the name of the interceptor. - * @param t The i18n function. - */ - name: (t: ReturnType) => MaybeRef - - /** - * Defines whether the interceptor has support for cookies. - * If this field is undefined, it is assumed as *not supporting* cookies. - */ - supportsCookies?: boolean - - /** - * Defines whether the interceptor has support for Digest Auth. - * If this field is undefined, it is assumed as *not supporting* the Digest Auth type. - */ - supportsDigestAuth?: boolean - - /** - * Defines whether the interceptor has support for Binary (file) content type. - * If this field is undefined, it is assumed as *supporting* the Binary content type. - */ - supportsBinaryContentType?: boolean - - /** - * Defines what to render in the Interceptor section of the Settings page. - * Use this space to define interceptor specific settings. - * Not setting this will lead to nothing being rendered about this interceptor in the settings page. - */ - settingsPageEntry?: { - /** - * The title of the interceptor entry in the settings page. - */ - entryTitle: (t: ReturnType) => string - - /** - * The component to render in the settings page. - */ - component: Component - } - - /** - * Defines what to render under the entry for the interceptor in the Interceptor selector. - */ - selectorSubtitle?: Component - - /** - * Defines whether the interceptor is selectable or not. - */ - selectable: MaybeRef> - - /** - * Runs the interceptor on the given request. - * NOTE: Make sure this function doesn't throw, instead when an error occurs, return a Left Either with the error. - * @param request The request to run the interceptor on. - */ - runRequest: (request: AxiosRequestConfig) => RequestRunResult -} - -/** - * This service deals with the registration and execution of - * interceptors for request execution. - */ -export class InterceptorService extends Service { - public static readonly ID = "INTERCEPTOR_SERVICE" - - private interceptors: Map = reactive(new Map()) - - /** - * The ID of the currently selected interceptor. - * If `null`, there are no interceptors registered or none can be selected. - */ - public currentInterceptorID: Ref = refWithControl( - null as string | null, - { - onBeforeChange: (value) => { - if (!value) { - // Only allow `null` if there are no interceptors - return this.availableInterceptors.value.length === 0 - } - - if (value && !this.interceptors.has(value)) { - console.warn( - "Attempt to set current interceptor ID to unknown ID is ignored" - ) - return false - } - - return true - }, - } - ) - - /** - * List of interceptors that are registered with the service. - */ - public availableInterceptors = computed(() => - Array.from(this.interceptors.values()) - ) - - /** - * Gives an instance to the current interceptor. - * NOTE: Do not update from here, this is only for reading. - */ - public currentInterceptor = computed(() => { - if (this.currentInterceptorID.value === null) return null - - return this.interceptors.get(this.currentInterceptorID.value) - }) - - override onServiceInit() { - // If the current interceptor is unselectable, select the first selectable one, else null - watch([() => this.interceptors, this.currentInterceptorID], () => { - if (!this.currentInterceptorID.value) return - - const interceptor = this.interceptors.get(this.currentInterceptorID.value) - - if (!interceptor) { - this.currentInterceptorID.value = null - return - } - - if (unref(interceptor.selectable).type === "unselectable") { - this.currentInterceptorID.value = - this.availableInterceptors.value.filter( - (interceptor) => unref(interceptor.selectable).type === "selectable" - )[0]?.interceptorID ?? null - } - }) - } - - /** - * Register an interceptor with the service. - * @param interceptor The interceptor to register - */ - public registerInterceptor(interceptor: Interceptor) { - // markRaw so that interceptor state by itself is not fully marked reactive - this.interceptors.set(interceptor.interceptorID, markRaw(interceptor)) - - if (this.currentInterceptorID.value === null) { - this.currentInterceptorID.value = interceptor.interceptorID - } - } - - /** - * Runs a request through the currently selected interceptor. - * @param req The request to run - * @throws If no interceptor is selected - */ - public runRequest(req: AxiosRequestConfig): RequestRunResult { - if (!this.currentInterceptorID.value) { - throw new Error("No interceptor selected") - } - - const interceptor = - this.interceptors.get(this.currentInterceptorID.value) ?? - throwError( - "Current Interceptor ID is not found in the list of registered interceptors" - ) - - return interceptor.runRequest(req) - } - - public static convertArrayBufferToString(data: ArrayBuffer): string { - return new TextDecoder().decode(data).replace(/\0+$/, "") - } -} diff --git a/packages/hoppscotch-common/src/services/persistence/__tests__/index.spec.ts b/packages/hoppscotch-common/src/services/persistence/__tests__/index.spec.ts index ef94375c..0f651f03 100644 --- a/packages/hoppscotch-common/src/services/persistence/__tests__/index.spec.ts +++ b/packages/hoppscotch-common/src/services/persistence/__tests__/index.spec.ts @@ -292,14 +292,14 @@ describe("PersistenceService", () => { it(`shows an error and sets the entry as a backup in localStorage if "${vuexKey}" read from localStorage doesn't match the schema`, async () => { // Invalid shape for `vuex` - // `postwoman.settings.CURRENT_INTERCEPTOR_ID` -> `string` + // `postwoman.settings.CURRENT_KERNEL_INTERCEPTOR_ID` -> should be `string`, not `number` const vuexData = { ...VUEX_DATA_MOCK, postwoman: { ...VUEX_DATA_MOCK.postwoman, settings: { ...VUEX_DATA_MOCK.postwoman.settings, - CURRENT_INTERCEPTOR_ID: 1234, + CURRENT_KERNEL_INTERCEPTOR_ID: 1234, }, }, } diff --git a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts index 1dbd8957..a6592513 100644 --- a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts @@ -36,7 +36,6 @@ const SettingsDefSchema = z.object({ syncHistory: z.boolean(), syncEnvironments: z.boolean(), PROXY_URL: z.string(), - CURRENT_INTERCEPTOR_ID: z.string(), CURRENT_KERNEL_INTERCEPTOR_ID: z.string(), URL_EXCLUDES: z.object({ auth: z.boolean(), diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/interceptor.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/interceptor.searcher.ts deleted file mode 100644 index 9efcca70..00000000 --- a/packages/hoppscotch-common/src/services/spotlight/searchers/interceptor.searcher.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Ref, computed, effectScope, markRaw, ref, unref, watch } from "vue" -import { getI18n } from "~/modules/i18n" -import { - SpotlightSearcher, - SpotlightSearcherResult, - SpotlightSearcherSessionState, - SpotlightService, -} from ".." - -import { Service } from "dioc" -import MiniSearch from "minisearch" -import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue" -import { InterceptorService } from "~/services/interceptor.service" -import IconCircle from "~icons/lucide/circle" - -/** - * This searcher is responsible for searching through the interceptor. - * And switching between them. - */ -export class InterceptorSpotlightSearcherService - extends Service - implements SpotlightSearcher -{ - public static readonly ID = "INTERCEPTOR_SPOTLIGHT_SEARCHER_SERVICE" - - private t = getI18n() - - public searcherID = "interceptor" - public searcherSectionTitle = this.t("settings.interceptor") - - private readonly spotlight = this.bind(SpotlightService) - private interceptorService = this.bind(InterceptorService) - - override onServiceInit() { - this.spotlight.registerSearcher(this) - } - - createSearchSession( - query: Readonly> - ): [Ref, () => void] { - const loading = ref(false) - const results = ref([]) - - const minisearch = new MiniSearch({ - fields: ["name", "alternates"], - storeFields: ["name"], - }) - - const interceptorSelection = this.interceptorService - .currentInterceptorID as Ref - - const interceptors = this.interceptorService.availableInterceptors - - minisearch.addAll( - interceptors.value.map((entry) => { - let id = `interceptor-${entry.interceptorID}` - if (entry.interceptorID === interceptorSelection.value) { - id += "-selected" - } - const name = unref(entry.name(this.t)) - return { - id, - name, - alternates: ["interceptor", "change", name], - } - }) - ) - - const scopeHandle = effectScope() - - scopeHandle.run(() => { - watch( - [query], - ([query]) => { - results.value = minisearch - .search(query, { - prefix: true, - fuzzy: true, - boost: { - reltime: 2, - }, - weights: { - fuzzy: 0.2, - prefix: 0.8, - }, - }) - .map((x) => { - return { - id: x.id, - icon: markRaw( - x.id.endsWith("-selected") ? IconCheckCircle : IconCircle - ), - score: x.score, - text: { - type: "text", - text: [this.t("spotlight.section.interceptor"), x.name], - }, - } - }) - }, - { immediate: true } - ) - }) - - const onSessionEnd = () => { - scopeHandle.stop() - minisearch.removeAll() - } - - const resultObj = computed(() => ({ - loading: loading.value, - results: results.value, - })) - - return [resultObj, onSessionEnd] - } - - onResultSelect(result: SpotlightSearcherResult): void { - const selectedInterceptor = result.id.split("-")[1] - this.interceptorService.currentInterceptorID.value = selectedInterceptor - } -}