diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index 35450a5fb..fd5eb94dc 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -1,5 +1,6 @@
{
"action": {
+ "add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
@@ -54,9 +55,28 @@
"new": "Add new",
"star": "Add star"
},
+ "cookies": {
+ "modal": {
+ "new_domain_name": "New domain name",
+ "set": "Set a cookie",
+ "cookie_string": "Cookie string",
+ "enter_cookie_string": "Enter cookie string",
+ "cookie_name": "Name",
+ "cookie_value": "Value",
+ "cookie_path": "Path",
+ "cookie_expires": "Expires",
+ "managed_tab": "Managed",
+ "raw_tab": "Raw",
+ "interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
+ "empty_domains": "Domain list is empty",
+ "empty_domain": "Domain is empty",
+ "no_cookies_in_domain": "No cookies set for this domain"
+ }
+ },
"app": {
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
+ "cookies": "Cookies",
"copy": "Copy",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
@@ -764,7 +784,7 @@
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect",
- "show":"Show",
+ "show": "Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
diff --git a/packages/hoppscotch-common/package.json b/packages/hoppscotch-common/package.json
index 30fe6ed4c..8f6cd0d24 100644
--- a/packages/hoppscotch-common/package.json
+++ b/packages/hoppscotch-common/package.json
@@ -52,6 +52,7 @@
"acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"buffer": "^6.0.3",
+ "cookie-es": "^1.0.0",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
@@ -76,6 +77,8 @@
"process": "^0.11.10",
"qs": "^6.11.2",
"rxjs": "^7.8.1",
+ "set-cookie-parser": "^2.6.0",
+ "set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
@@ -98,7 +101,8 @@
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.5.0",
- "yargs-parser": "^21.1.1"
+ "yargs-parser": "^21.1.1",
+ "zod": "^3.22.2"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts
index f7d8352a2..786d61c73 100644
--- a/packages/hoppscotch-common/src/components.d.ts
+++ b/packages/hoppscotch-common/src/components.d.ts
@@ -1,11 +1,11 @@
-/* eslint-disable */
-/* prettier-ignore */
-// @ts-nocheck
-// Generated by unplugin-vue-components
+// generated by unplugin-vue-components
+// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
export {}
-declare module 'vue' {
+declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -58,6 +58,8 @@ declare module 'vue' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
+ CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
+ CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -90,13 +92,11 @@ declare module 'vue' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
- HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
- HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -143,7 +143,6 @@ declare module 'vue' {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
- IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -153,10 +152,8 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
- IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
- IconLucideVerified: typeof import('~icons/lucide/verified')['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']
@@ -189,7 +186,6 @@ declare module 'vue' {
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
- SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
@@ -222,4 +218,5 @@ declare module 'vue' {
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
}
+
}
diff --git a/packages/hoppscotch-common/src/components/app/Footer.vue b/packages/hoppscotch-common/src/components/app/Footer.vue
index fd44e7708..a2ca305ad 100644
--- a/packages/hoppscotch-common/src/components/app/Footer.vue
+++ b/packages/hoppscotch-common/src/components/app/Footer.vue
@@ -20,6 +20,12 @@
+
+
diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue
index c903a068f..a19370048 100644
--- a/packages/hoppscotch-common/src/components/collections/index.vue
+++ b/packages/hoppscotch-common/src/components/collections/index.vue
@@ -1866,28 +1866,25 @@ const getJSONCollection = async () => {
* @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name
*/
-const initializeDownloadCollection = (
+const initializeDownloadCollection = async (
collectionJSON: string,
name: string | null
) => {
- const file = new Blob([collectionJSON], { type: "application/json" })
- const a = document.createElement("a")
- const url = URL.createObjectURL(file)
- a.href = url
+ const result = await platform.io.saveFileWithDialog({
+ data: collectionJSON,
+ contentType: "application/json",
+ suggestedFilename: `${name ?? "collection"}.json`,
+ filters: [
+ {
+ name: "Hoppscotch Collection JSON file",
+ extensions: ["json"],
+ },
+ ],
+ })
- if (name) {
- a.download = `${name}.json`
- } else {
- a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
+ if (result.type === "unknown" || result.type === "saved") {
+ toast.success(t("state.download_started").toString())
}
-
- document.body.appendChild(a)
- a.click()
- toast.success(t("state.download_started").toString())
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- }, 1000)
}
/**
@@ -1916,11 +1913,14 @@ const exportData = async (
exportLoading.value = false
return
},
- (coll) => {
+ async (coll) => {
const hoppColl = teamCollToHoppRESTColl(coll)
const collectionJSONString = JSON.stringify(hoppColl)
- initializeDownloadCollection(collectionJSONString, hoppColl.name)
+ await initializeDownloadCollection(
+ collectionJSONString,
+ hoppColl.name
+ )
exportLoading.value = false
}
)
diff --git a/packages/hoppscotch-common/src/components/cookies/AllModal.vue b/packages/hoppscotch-common/src/components/cookies/AllModal.vue
new file mode 100644
index 000000000..80aed5b43
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/cookies/AllModal.vue
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("cookies.modal.no_cookies_in_domain") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/cookies/EditCookie.vue b/packages/hoppscotch-common/src/components/cookies/EditCookie.vue
new file mode 100644
index 000000000..d72a56a67
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/cookies/EditCookie.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/environments/ImportExport.vue b/packages/hoppscotch-common/src/components/environments/ImportExport.vue
index a84461d3e..4fa07b55e 100644
--- a/packages/hoppscotch-common/src/components/environments/ImportExport.vue
+++ b/packages/hoppscotch-common/src/components/environments/ImportExport.vue
@@ -375,7 +375,7 @@ const importFromPostman = ({
importFromHoppscotch(environments)
}
-const exportJSON = () => {
+const exportJSON = async () => {
const dataToWrite = environmentJson.value
const parsedCollections = JSON.parse(dataToWrite)
@@ -385,19 +385,27 @@ const exportJSON = () => {
}
const file = new Blob([dataToWrite], { type: "application/json" })
- const a = document.createElement("a")
const url = URL.createObjectURL(file)
- a.href = url
- // TODO: get uri from meta
- a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
- document.body.appendChild(a)
- a.click()
- toast.success(t("state.download_started").toString())
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- }, 1000)
+ const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
+
+ URL.revokeObjectURL(url)
+
+ const result = await platform.io.saveFileWithDialog({
+ data: dataToWrite,
+ contentType: "application/json",
+ suggestedFilename: filename,
+ filters: [
+ {
+ name: "JSON file",
+ extensions: ["json"],
+ },
+ ],
+ })
+
+ if (result.type === "unknown" || result.type === "saved") {
+ toast.success(t("state.download_started").toString())
+ }
}
const getErrorMessage = (err: GQLError) => {
diff --git a/packages/hoppscotch-common/src/components/graphql/Response.vue b/packages/hoppscotch-common/src/components/graphql/Response.vue
index 76f7ae65f..54010711a 100644
--- a/packages/hoppscotch-common/src/components/graphql/Response.vue
+++ b/packages/hoppscotch-common/src/components/graphql/Response.vue
@@ -59,6 +59,7 @@ import { useToast } from "@composables/toast"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
+import { platform } from "~/platform"
const t = useI18n()
const toast = useToast()
@@ -111,21 +112,31 @@ const copyResponse = (str: string) => {
toast.success(`${t("state.copied_to_clipboard")}`)
}
-const downloadResponse = (str: string) => {
+const downloadResponse = async (str: string) => {
const dataToWrite = str
const file = new Blob([dataToWrite!], { type: "application/json" })
- const a = document.createElement("a")
const url = URL.createObjectURL(file)
- a.href = url
- a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
- document.body.appendChild(a)
- a.click()
- downloadResponseIcon.value = IconCheck
- toast.success(`${t("state.download_started")}`)
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- }, 1000)
+
+ const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
+
+ URL.revokeObjectURL(url)
+
+ const result = await platform.io.saveFileWithDialog({
+ data: dataToWrite,
+ contentType: "application/json",
+ suggestedFilename: filename,
+ filters: [
+ {
+ name: "JSON file",
+ extensions: ["json"],
+ },
+ ],
+ })
+
+ if (result.type === "unknown" || result.type === "saved") {
+ downloadResponseIcon.value = IconCheck
+ toast.success(`${t("state.download_started")}`)
+ }
}
defineActionHandler(
diff --git a/packages/hoppscotch-common/src/components/graphql/Sidebar.vue b/packages/hoppscotch-common/src/components/graphql/Sidebar.vue
index b297814ab..fbf51e047 100644
--- a/packages/hoppscotch-common/src/components/graphql/Sidebar.vue
+++ b/packages/hoppscotch-common/src/components/graphql/Sidebar.vue
@@ -202,6 +202,7 @@ import {
schemaString,
subscriptionFields,
} from "~/helpers/graphql/connection"
+import { platform } from "~/platform"
type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
@@ -372,21 +373,33 @@ useCodemirror(
})
)
-const downloadSchema = () => {
- const dataToWrite = JSON.stringify(schemaString.value, null, 2)
+const downloadSchema = async () => {
+ const dataToWrite = schemaString.value
const file = new Blob([dataToWrite], { type: "application/graphql" })
- const a = document.createElement("a")
const url = URL.createObjectURL(file)
- a.href = url
- a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
- document.body.appendChild(a)
- a.click()
- downloadSchemaIcon.value = IconCheck
- toast.success(`${t("state.download_started")}`)
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- }, 1000)
+
+ const filename = `${
+ url.split("/").pop()!.split("#")[0].split("?")[0]
+ }.graphql`
+
+ URL.revokeObjectURL(url)
+
+ const result = await platform.io.saveFileWithDialog({
+ data: dataToWrite,
+ contentType: "application/graphql",
+ suggestedFilename: filename,
+ filters: [
+ {
+ name: "GraphQL Schema File",
+ extensions: ["graphql"],
+ },
+ ],
+ })
+
+ if (result.type === "unknown" || result.type === "saved") {
+ downloadSchemaIcon.value = IconCheck
+ toast.success(`${t("state.download_started")}`)
+ }
}
const copySchema = () => {
diff --git a/packages/hoppscotch-common/src/composables/lens-actions.ts b/packages/hoppscotch-common/src/composables/lens-actions.ts
index c2f22a614..160365c9a 100644
--- a/packages/hoppscotch-common/src/composables/lens-actions.ts
+++ b/packages/hoppscotch-common/src/composables/lens-actions.ts
@@ -10,6 +10,7 @@ import { useI18n } from "./i18n"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
+import { platform } from "~/platform"
export function useCopyResponse(responseBodyText: Ref) {
const toast = useToast()
@@ -40,15 +41,14 @@ export function useDownloadResponse(
const toast = useToast()
const t = useI18n()
- const downloadResponse = () => {
+ const downloadResponse = async () => {
const dataToWrite = responseBody.value
- const file = new Blob([dataToWrite], { type: contentType })
- const a = document.createElement("a")
- const url = URL.createObjectURL(file)
- a.href = url
- // TODO: get uri from meta
- a.download = pipe(
+ // Guess extension and filename
+ const file = new Blob([dataToWrite], { type: contentType })
+ const url = URL.createObjectURL(file)
+
+ const filename = pipe(
url,
S.split("/"),
RNEA.last,
@@ -58,15 +58,24 @@ export function useDownloadResponse(
RNEA.head
)
- document.body.appendChild(a)
- a.click()
- downloadIcon.value = IconCheck
- toast.success(`${t("state.download_started")}`)
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- }, 1000)
+ URL.revokeObjectURL(url)
+
+ console.log(filename)
+
+ // TODO: Look at the mime type and determine extension ?
+ const result = await platform.io.saveFileWithDialog({
+ data: dataToWrite,
+ contentType: contentType,
+ suggestedFilename: filename,
+ })
+
+ // Assume success if unknown as we cannot determine
+ if (result.type === "unknown" || result.type === "saved") {
+ downloadIcon.value = IconCheck
+ toast.success(`${t("state.download_started")}`)
+ }
}
+
return {
downloadIcon,
downloadResponse,
diff --git a/packages/hoppscotch-common/src/helpers/import-export/export/environment.ts b/packages/hoppscotch-common/src/helpers/import-export/export/environment.ts
index 5bc6a42e0..82c812e57 100644
--- a/packages/hoppscotch-common/src/helpers/import-export/export/environment.ts
+++ b/packages/hoppscotch-common/src/helpers/import-export/export/environment.ts
@@ -1,6 +1,7 @@
import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { cloneDeep } from "lodash-es"
+import { platform } from "~/platform"
const getEnvironmentJson = (
environmentObj: TeamEnvironment | Environment,
@@ -32,17 +33,24 @@ export const exportAsJSON = (
if (!dataToWrite) return false
const file = new Blob([dataToWrite], { type: "application/json" })
- const a = document.createElement("a")
const url = URL.createObjectURL(file)
- a.href = url
- // Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
- a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
- document.body.appendChild(a)
- a.click()
- setTimeout(() => {
- document.body.removeChild(a)
- window.URL.revokeObjectURL(url)
- }, 0)
+ URL.revokeObjectURL(url)
+
+ platform.io.saveFileWithDialog({
+ data: dataToWrite,
+ contentType: "application/json",
+ // Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
+ suggestedFilename: `${
+ url.split("/").pop()!.split("#")[0].split("?")[0]
+ }.json`,
+ filters: [
+ {
+ name: "JSON file",
+ extensions: ["json"],
+ },
+ ],
+ })
+
return true
}
diff --git a/packages/hoppscotch-common/src/newstore/localpersistence.ts b/packages/hoppscotch-common/src/newstore/localpersistence.ts
index e3308df8d..1911f0053 100644
--- a/packages/hoppscotch-common/src/newstore/localpersistence.ts
+++ b/packages/hoppscotch-common/src/newstore/localpersistence.ts
@@ -47,6 +47,9 @@ import { StorageLike, watchDebounced } from "@vueuse/core"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
+import { z } from "zod"
+import { CookieJarService } from "~/services/cookie-jar.service"
+import { watch } from "vue"
function checkAndMigrateOldSettings() {
if (window.localStorage.getItem("selectedEnvIndex")) {
@@ -182,6 +185,35 @@ function setupHistoryPersistence() {
})
}
+const cookieSchema = z.record(z.array(z.string()))
+
+function setupCookiesPersistence() {
+ const cookieJarService = getService(CookieJarService)
+
+ try {
+ const cookieData = JSON.parse(
+ window.localStorage.getItem("cookieJar") || "{}"
+ )
+
+ const parseResult = cookieSchema.safeParse(cookieData)
+
+ if (parseResult.success) {
+ for (const domain in parseResult.data) {
+ cookieJarService.bulkApplyCookiesToDomain(
+ parseResult.data[domain],
+ domain
+ )
+ }
+ }
+ } catch (e) {}
+
+ watch(cookieJarService.cookieJar, (cookieJar) => {
+ const data = JSON.stringify(Object.fromEntries(cookieJar.entries()))
+
+ window.localStorage.setItem("cookieJar", data)
+ })
+}
+
function setupCollectionsPersistence() {
const restCollectionData = JSON.parse(
window.localStorage.getItem("collections") || "[]"
@@ -382,6 +414,8 @@ export function setupLocalPersistence() {
setupSocketIOPersistence()
setupSSEPersistence()
setupMQTTPersistence()
+
+ setupCookiesPersistence()
}
/**
diff --git a/packages/hoppscotch-common/src/platform/index.ts b/packages/hoppscotch-common/src/platform/index.ts
index 22cbaf112..e527d6174 100644
--- a/packages/hoppscotch-common/src/platform/index.ts
+++ b/packages/hoppscotch-common/src/platform/index.ts
@@ -9,12 +9,14 @@ import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
+import { IOPlatformDef } from "./io"
export type PlatformDef = {
ui?: UIPlatformDef
addedHoppModules?: HoppModule[]
auth: AuthPlatformDef
analytics?: AnalyticsPlatformDef
+ io: IOPlatformDef
sync: {
environments: EnvironmentsPlatformDef
collections: CollectionsPlatformDef
@@ -27,6 +29,12 @@ export type PlatformDef = {
platformFeatureFlags: {
exportAsGIST: boolean
hasTelemetry: boolean
+
+ /**
+ * Whether the platform supports cookies (affects whether the cookies footer item is shown)
+ * If a value is not given, then the value is assumed to be false
+ */
+ cookiesEnabled?: boolean
}
}
diff --git a/packages/hoppscotch-common/src/platform/io.ts b/packages/hoppscotch-common/src/platform/io.ts
new file mode 100644
index 000000000..97d32a7a5
--- /dev/null
+++ b/packages/hoppscotch-common/src/platform/io.ts
@@ -0,0 +1,84 @@
+/**
+ * Defines how to save a file to the user's filesystem.
+ */
+export type SaveFileWithDialogOptions = {
+ /**
+ * The data to be saved
+ */
+ data: string | ArrayBuffer
+
+ /**
+ * The suggested filename for the file. This name will be shown in the
+ * save dialog by default when a save is initiated.
+ */
+ suggestedFilename: string
+
+ /**
+ * The content type mime type of the data to be saved.
+ *
+ * NOTE: The usage of this data might be platform dependent.
+ * For example, this field is used in the web, but not in the desktop app.
+ */
+ contentType: string
+
+ /**
+ * Defines the filters (like in Windows, on the right side, where you can
+ * select the file type) for the file dialog.
+ *
+ * NOTE: The usage of this data might be platform dependent.
+ * For example, this field is used in the web, but not in the desktop app.
+ */
+ filters?: Array<{
+ /**
+ * The name of the filter (in Windows, if the filter looks
+ * like "Images (*.png, *.jpg)", the name would be "Images")
+ */
+ name: string
+
+ /**
+ * The array of extensions that are supported, without the dot.
+ */
+ extensions: string[]
+ }>
+}
+
+export type SaveFileResponse =
+ | {
+ /**
+ * The implementation was unable to determine the status of the save operation.
+ * This cannot be considered a success or a failure and should be handled as an uncertainity.
+ * The browser standard implementation (std) returns this value as there is no way to
+ * check if the user downloaded the file or not.
+ */
+ type: "unknown"
+ }
+ | {
+ /**
+ * The result is known and the user cancelled the save.
+ */
+ type: "cancelled"
+ }
+ | {
+ /**
+ * The result is known and the user saved the file.
+ */
+ type: "saved"
+
+ /**
+ * The full path of where the file was saved
+ */
+ path: string
+ }
+
+/**
+ * Platform definitions for how to handle IO operations.
+ */
+export type IOPlatformDef = {
+ /**
+ * Defines how to save a file to the user's filesystem.
+ * The expected behaviour is for the browser to show a prompt to save the file.
+ */
+ saveFileWithDialog: (
+ opts: SaveFileWithDialogOptions
+ ) => Promise
+}
diff --git a/packages/hoppscotch-common/src/platform/std/io.ts b/packages/hoppscotch-common/src/platform/std/io.ts
new file mode 100644
index 000000000..568a7ecbf
--- /dev/null
+++ b/packages/hoppscotch-common/src/platform/std/io.ts
@@ -0,0 +1,37 @@
+import { IOPlatformDef } from "../io"
+import { pipe } from "fp-ts/function"
+import * as S from "fp-ts/string"
+import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
+
+/**
+ * Implementation for how to handle IO operations in the browser.
+ */
+export const browserIODef: IOPlatformDef = {
+ saveFileWithDialog(opts) {
+ const file = new Blob([opts.data], { type: opts.contentType })
+ const a = document.createElement("a")
+ const url = URL.createObjectURL(file)
+
+ a.href = url
+ a.download = pipe(
+ url,
+ S.split("/"),
+ RNEA.last,
+ S.split("#"),
+ RNEA.head,
+ S.split("?"),
+ RNEA.head
+ )
+
+ document.body.appendChild(a)
+ a.click()
+
+ setTimeout(() => {
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ }, 1000)
+
+ // Browsers provide no way for us to know the save went successfully.
+ return Promise.resolve({ type: "unknown" })
+ },
+}
diff --git a/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts b/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts
index 35c21b33a..0c060fcce 100644
--- a/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts
+++ b/packages/hoppscotch-common/src/services/__tests__/interceptor.service.spec.ts
@@ -72,6 +72,61 @@ describe("InterceptorService", () => {
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()
diff --git a/packages/hoppscotch-common/src/services/cookie-jar.service.ts b/packages/hoppscotch-common/src/services/cookie-jar.service.ts
new file mode 100644
index 000000000..b41340a7a
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/cookie-jar.service.ts
@@ -0,0 +1,69 @@
+import { Service } from "dioc"
+import { ref } from "vue"
+import { parseString as setCookieParse } from "set-cookie-parser-es"
+
+export type CookieDef = {
+ name: string
+ value: string
+ domain: string
+ path: string
+ expires: string
+}
+
+export class CookieJarService extends Service {
+ public static readonly ID = "COOKIE_JAR_SERVICE"
+
+ /**
+ * The cookie jar that stores all relevant cookie info.
+ * The keys correspond to the domain of the cookie.
+ * The cookie strings are stored as an array of strings corresponding to the domain
+ */
+ public cookieJar = ref(new Map())
+
+ constructor() {
+ super()
+ }
+
+ public parseSetCookieString(setCookieString: string) {
+ return setCookieParse(setCookieString)
+ }
+
+ public bulkApplyCookiesToDomain(cookies: string[], domain: string) {
+ const existingDomainEntries = this.cookieJar.value.get(domain) ?? []
+ existingDomainEntries.push(...cookies)
+
+ this.cookieJar.value.set(domain, existingDomainEntries)
+ }
+
+ public getCookiesForURL(url: URL) {
+ const relevantDomains = Array.from(this.cookieJar.value.keys()).filter(
+ (domain) => url.hostname.endsWith(domain)
+ )
+
+ return relevantDomains
+ .flatMap((domain) => {
+ // Assemble the list of cookie entries from all the relevant domains
+
+ const cookieStrings = this.cookieJar.value.get(domain)! // We know not nullable from how we filter above
+
+ return cookieStrings.map((cookieString) =>
+ this.parseSetCookieString(cookieString)
+ )
+ })
+ .filter((cookie) => {
+ // Perform the required checks on the cookies
+
+ const passesPathCheck = url.pathname.startsWith(cookie.path ?? "/")
+
+ const passesExpiresCheck = !cookie.expires
+ ? true
+ : cookie.expires.getTime() >= new Date().getTime()
+
+ const passesSecureCheck = !cookie.secure
+ ? true
+ : url.protocol === "https:"
+
+ return passesPathCheck && passesExpiresCheck && passesSecureCheck
+ })
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/interceptor.service.ts b/packages/hoppscotch-common/src/services/interceptor.service.ts
index 9a3a2662e..4ed5b113e 100644
--- a/packages/hoppscotch-common/src/services/interceptor.service.ts
+++ b/packages/hoppscotch-common/src/services/interceptor.service.ts
@@ -85,6 +85,12 @@ export type Interceptor = {
*/
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 what to render in the Interceptor section of the Settings page.
* Use this space to define interceptor specific settings.
@@ -161,6 +167,16 @@ export class InterceptorService extends Service {
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)
+ })
+
constructor() {
super()
diff --git a/packages/hoppscotch-selfhost-desktop/.gitignore b/packages/hoppscotch-selfhost-desktop/.gitignore
new file mode 100644
index 000000000..358c8d9a8
--- /dev/null
+++ b/packages/hoppscotch-selfhost-desktop/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Sitemap
+.sitemap-gen
+
+# Backend Code generation
+src/api/generated
diff --git a/packages/hoppscotch-selfhost-desktop/.vscode/extensions.json b/packages/hoppscotch-selfhost-desktop/.vscode/extensions.json
new file mode 100644
index 000000000..cf4385bdd
--- /dev/null
+++ b/packages/hoppscotch-selfhost-desktop/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "Vue.volar",
+ "tauri-apps.tauri-vscode",
+ "rust-lang.rust-analyzer"
+ ]
+}
diff --git a/packages/hoppscotch-selfhost-desktop/README.md b/packages/hoppscotch-selfhost-desktop/README.md
new file mode 100644
index 000000000..e6b0bd5e8
--- /dev/null
+++ b/packages/hoppscotch-selfhost-desktop/README.md
@@ -0,0 +1,16 @@
+# Tauri + Vue 3 + TypeScript
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
+
+
+