diff --git a/packages/hoppscotch-common/src/components/settings/Proxy.vue b/packages/hoppscotch-common/src/components/settings/Proxy.vue index 6c136ea5..5a2f92f1 100644 --- a/packages/hoppscotch-common/src/components/settings/Proxy.vue +++ b/packages/hoppscotch-common/src/components/settings/Proxy.vue @@ -31,20 +31,17 @@ diff --git a/packages/hoppscotch-common/src/newstore/settings.ts b/packages/hoppscotch-common/src/newstore/settings.ts index bdfa4458..cb05f924 100644 --- a/packages/hoppscotch-common/src/newstore/settings.ts +++ b/packages/hoppscotch-common/src/newstore/settings.ts @@ -3,7 +3,7 @@ import { Observable } from "rxjs" import { distinctUntilChanged, pluck } from "rxjs/operators" import type { KeysMatching } from "~/types/ts-utils" import DispatchingStore, { defineDispatchers } from "./DispatchingStore" -import { DEFAULT_HOPP_PROXY_URL, getDefaultProxyUrl } from "~/helpers/proxyUrl" +import { DEFAULT_HOPP_PROXY_URL } from "~/helpers/proxyUrl" export const HoppBgColors = ["system", "light", "dark", "black"] as const @@ -87,12 +87,6 @@ export type SettingsDef = { ENABLE_EXPERIMENTAL_DOCUMENTATION: boolean } -let defaultProxyURL = DEFAULT_HOPP_PROXY_URL - -getDefaultProxyUrl().then((url) => { - defaultProxyURL = url -}) - export const getDefaultSettings = (): SettingsDef => { return { syncCollections: true, @@ -122,8 +116,10 @@ export const getDefaultSettings = (): SettingsDef => { // Set empty because interceptor module will set the default value CURRENT_KERNEL_INTERCEPTOR_ID: "", - // TODO: Interceptor related settings should move under the interceptor systems - PROXY_URL: defaultProxyURL, + // Static fallback, the actual platform-aware proxy URL is managed by + // `KernelInterceptorProxyStore` which resolves it at runtime via + // `getDefaultProxyUrl()` (after the platform is initialised). + PROXY_URL: DEFAULT_HOPP_PROXY_URL, URL_EXCLUDES: { auth: true, httpUser: true, diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts index f70308a2..ffe62887 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts @@ -1,12 +1,13 @@ +import { ref, readonly, toRaw, type Ref, type DeepReadonly } from "vue" import { Service } from "dioc" import { Store } from "~/kernel/store" -import { settingsStore } from "~/newstore/settings" +import { getDefaultProxyUrl, DEFAULT_HOPP_PROXY_URL } from "~/helpers/proxyUrl" import * as E from "fp-ts/Either" const STORE_NAMESPACE = "interceptors.proxy.v1" const SETTINGS_KEY = "settings" -type ProxySettings = { +export type ProxySettings = { version: "v1" proxyUrl: string accessToken: string @@ -18,16 +19,41 @@ interface StoredData { lastUpdated: string } -const DEFAULT_SETTINGS: ProxySettings = { - version: "v1", - proxyUrl: settingsStore.value.PROXY_URL ?? "https://proxy.hoppscotch.io", - accessToken: import.meta.env.VITE_PROXYSCOTCH_ACCESS_TOKEN ?? "", +/** + * Build fresh default settings. + * Called as a function (not a static const) so that `getDefaultProxyUrl()` + * can resolve against the current platform, which isn't available at + * module-load time. + */ +async function buildDefaultSettings(): Promise { + return { + version: "v1", + proxyUrl: await getDefaultProxyUrl(), + accessToken: import.meta.env.VITE_PROXYSCOTCH_ACCESS_TOKEN ?? "", + } } +/** + * Reactive proxy settings store. + * + * Exposes `settings$` as a readonly ref, any component or service that reads + * `settings$.value` will automatically re-render when settings change. + */ export class KernelInterceptorProxyStore extends Service { public static readonly ID = "KERNEL_PROXY_INTERCEPTOR_STORE" - private settings: ProxySettings = { ...DEFAULT_SETTINGS } + private readonly _settings = ref({ + version: "v1", + proxyUrl: DEFAULT_HOPP_PROXY_URL, + accessToken: import.meta.env.VITE_PROXYSCOTCH_ACCESS_TOKEN ?? "", + }) + + /** + * Reactive, read-only view of the current proxy settings. + */ + public readonly settings$: DeepReadonly> = readonly( + this._settings + ) async onServiceInit(): Promise { const initResult = await Store.init() @@ -42,7 +68,14 @@ export class KernelInterceptorProxyStore extends Service { watcher.on("change", async ({ value }) => { if (value) { const storedData = value as StoredData - this.settings = storedData.settings + this._settings.value = { + ...this._settings.value, + // Only sync user-configurable fields from external changes. + // Fallback to current value if persisted data is missing the field + // (e.g. older schema). accessToken stays env-derived. + proxyUrl: + storedData.settings?.proxyUrl ?? this._settings.value.proxyUrl, + } } }) } @@ -53,21 +86,29 @@ export class KernelInterceptorProxyStore extends Service { SETTINGS_KEY ) + const defaults = await buildDefaultSettings() + if (E.isRight(loadResult) && loadResult.right) { const storedData = loadResult.right - this.settings = { - ...DEFAULT_SETTINGS, - ...storedData.settings, + this._settings.value = { + ...defaults, + // Only restore user-configurable fields from storage. + // accessToken is env-derived (VITE_PROXYSCOTCH_ACCESS_TOKEN) and + // must always reflect the current deployment, not a stale persisted value. + proxyUrl: storedData.settings?.proxyUrl ?? defaults.proxyUrl, } } else { + this._settings.value = { ...defaults } await this.persistSettings() } } private async persistSettings(): Promise { + const rawSettings = toRaw(this._settings.value) + const storedData: StoredData = { version: "v1", - settings: this.settings, + settings: { ...rawSettings }, lastUpdated: new Date().toISOString(), } @@ -82,21 +123,31 @@ export class KernelInterceptorProxyStore extends Service { } } - public async updateSettings(settings: Partial): Promise { - this.settings = { - ...this.settings, - ...settings, + /** + * Update user-configurable proxy settings. + * Only `proxyUrl` is user-configurable, `accessToken` and `version` are + * derived at runtime and cannot be overwritten by callers. + */ + public async updateSettings( + patch: Pick + ): Promise { + this._settings.value = { + ...this._settings.value, + proxyUrl: patch.proxyUrl, } - await this.persistSettings() } + /** + * @deprecated Use `settings$` for reactive access. This exists only for + * non-reactive contexts (e.g. inside `execute()` in the interceptor service). + */ public getSettings(): ProxySettings { - return { ...this.settings } + return { ...this._settings.value } } public async resetSettings(): Promise { - this.settings = { ...DEFAULT_SETTINGS } + this._settings.value = await buildDefaultSettings() await this.persistSettings() } }