Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts: # clients/cli/package-lock.json # clients/cli/package.json # clients/web/src/App.tsx # clients/web/src/lib/environmentFactory.test.ts # clients/web/src/lib/environmentFactory.ts # clients/web/src/lib/remoteOAuthStorage.test.ts # clients/web/src/lib/remoteOAuthStorage.ts # clients/web/src/test/core/auth/connection-state.test.ts # clients/web/src/test/core/auth/providers.test.ts # clients/web/src/test/core/auth/storage-browser.test.ts # clients/web/src/test/core/auth/storage-remote.test.ts # clients/web/src/test/core/mcp/oauthManager.test.ts # clients/web/src/test/integration/auth/node/storage.test.ts # clients/web/src/test/integration/storage/adapters.test.ts # clients/web/src/utils/clearServerOAuthState.test.ts # clients/web/src/utils/clearServerOAuthState.ts # core/auth/connection-state.ts # core/auth/oauth-storage.ts # core/auth/providers.ts # core/auth/storage.ts # core/auth/store.ts # core/storage/adapters/remote-storage.ts
This commit is contained in:
commit
32a3610737
4 changed files with 5046 additions and 3271 deletions
|
|
@ -1,111 +1,124 @@
|
|||
/**
|
||||
* Tests for the browser `InspectorClientEnvironment` assembly.
|
||||
*
|
||||
* The three remote factories are mocked so we can (a) assert the `baseUrl` /
|
||||
* `authToken` derived from `window.location` are threaded into each, and
|
||||
* (b) capture the internal `fetchFn` wrapper and invoke it to prove it
|
||||
* delegates to `globalThis.fetch` (exercising the arrow body that exists to
|
||||
* preserve the global receiver). `BrowserNavigation` and the OAuth storage
|
||||
* accessor are the real implementations.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BrowserNavigation } from "@inspector/core/auth/browser/index.js";
|
||||
import type { RedirectUrlProvider } from "@inspector/core/auth/index.js";
|
||||
|
||||
interface CapturedOptions {
|
||||
baseUrl: string;
|
||||
authToken: string | undefined;
|
||||
fetchFn: typeof fetch;
|
||||
}
|
||||
|
||||
const captured: {
|
||||
transport?: CapturedOptions;
|
||||
fetch?: CapturedOptions;
|
||||
logger?: CapturedOptions;
|
||||
} = {};
|
||||
|
||||
vi.mock("@inspector/core/mcp/remote/index.js", () => ({
|
||||
createRemoteTransport: (opts: CapturedOptions) => {
|
||||
captured.transport = opts;
|
||||
return { transport: true };
|
||||
},
|
||||
createRemoteFetch: (opts: CapturedOptions) => {
|
||||
captured.fetch = opts;
|
||||
return (async () => new Response("ok")) as unknown as typeof fetch;
|
||||
},
|
||||
createRemoteLogger: (opts: CapturedOptions) => {
|
||||
captured.logger = opts;
|
||||
return { info: vi.fn() };
|
||||
},
|
||||
}));
|
||||
|
||||
import { MutableRedirectUrlProvider } from "@inspector/core/auth/providers.js";
|
||||
import { createWebEnvironment } from "./environmentFactory";
|
||||
|
||||
const REDIRECT: RedirectUrlProvider = {
|
||||
getRedirectUrl: () => "http://localhost/callback",
|
||||
};
|
||||
import {
|
||||
getWebRemoteOAuthStorage,
|
||||
resetWebRemoteOAuthStorageCacheForTests,
|
||||
} from "./remoteOAuthStorage";
|
||||
|
||||
describe("createWebEnvironment", () => {
|
||||
beforeEach(() => {
|
||||
captured.transport = undefined;
|
||||
captured.fetch = undefined;
|
||||
captured.logger = undefined;
|
||||
vi.stubGlobal("window", {
|
||||
location: {
|
||||
protocol: "http:",
|
||||
host: "127.0.0.1:6299",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resetWebRemoteOAuthStorageCacheForTests();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("threads the window.location origin and auth token into every remote factory", () => {
|
||||
const expectedBaseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const { environment, logger } = createWebEnvironment("tok-123", REDIRECT);
|
||||
it("wires RemoteOAuthStorage shared with getWebRemoteOAuthStorage", () => {
|
||||
const redirectUrlProvider = new MutableRedirectUrlProvider();
|
||||
const first = createWebEnvironment("unit-test-token", redirectUrlProvider);
|
||||
const second = createWebEnvironment("unit-test-token", redirectUrlProvider);
|
||||
|
||||
for (const opts of [captured.transport, captured.fetch, captured.logger]) {
|
||||
expect(opts?.baseUrl).toBe(expectedBaseUrl);
|
||||
expect(opts?.authToken).toBe("tok-123");
|
||||
}
|
||||
expect(first.environment.oauth).toBeDefined();
|
||||
expect(second.environment.oauth).toBeDefined();
|
||||
expect(second.environment.oauth!.storage).toBe(
|
||||
first.environment.oauth!.storage,
|
||||
);
|
||||
expect(first.environment.oauth!.storage).toBe(
|
||||
getWebRemoteOAuthStorage("unit-test-token"),
|
||||
);
|
||||
});
|
||||
|
||||
// The returned logger is the same instance the factory produced.
|
||||
it("uses BrowserNavigation for oauth.navigation", () => {
|
||||
const { environment } = createWebEnvironment(
|
||||
undefined,
|
||||
new MutableRedirectUrlProvider(),
|
||||
);
|
||||
expect(environment.oauth).toBeDefined();
|
||||
expect(environment.oauth!.navigation).toBeInstanceOf(BrowserNavigation);
|
||||
});
|
||||
|
||||
it("returns the same logger instance as environment.logger", () => {
|
||||
const { environment, logger } = createWebEnvironment(
|
||||
"tok",
|
||||
new MutableRedirectUrlProvider(),
|
||||
);
|
||||
expect(logger).toBe(environment.logger);
|
||||
expect(environment.transport).toEqual({ transport: true });
|
||||
expect(typeof environment.fetch).toBe("function");
|
||||
});
|
||||
|
||||
it("passes an undefined auth token straight through", () => {
|
||||
createWebEnvironment(undefined, REDIRECT);
|
||||
expect(captured.logger?.authToken).toBeUndefined();
|
||||
it("passes redirectUrlProvider into oauth config", () => {
|
||||
const redirectUrlProvider = new MutableRedirectUrlProvider();
|
||||
redirectUrlProvider.redirectUrl = "http://127.0.0.1:6299/oauth/callback";
|
||||
const { environment } = createWebEnvironment("tok", redirectUrlProvider);
|
||||
if (!environment.oauth) {
|
||||
throw new Error("expected oauth config");
|
||||
}
|
||||
const { redirectUrlProvider: oauthRedirect } = environment.oauth;
|
||||
if (!oauthRedirect) {
|
||||
throw new Error("expected redirectUrlProvider");
|
||||
}
|
||||
expect(oauthRedirect.getRedirectUrl()).toBe(
|
||||
"http://127.0.0.1:6299/oauth/callback",
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps fetch so the call preserves the global receiver", async () => {
|
||||
const spy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue(new Response("delegated", { status: 200 }));
|
||||
createWebEnvironment("tok", REDIRECT);
|
||||
|
||||
// Every factory receives the identical wrapper; invoking it must delegate.
|
||||
const wrapper = captured.transport!.fetchFn;
|
||||
const res = await wrapper("http://example.test/x");
|
||||
expect(spy).toHaveBeenCalledWith("http://example.test/x");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("builds a BrowserNavigation and wires the OAuth storage + redirect provider", () => {
|
||||
const onBeforeRedirect = vi.fn();
|
||||
it("forwards onBeforeOAuthRedirect to BrowserNavigation", () => {
|
||||
const onBeforeOAuthRedirect = vi.fn<(authorizationUrl: URL) => void>();
|
||||
const { environment } = createWebEnvironment(
|
||||
"tok",
|
||||
REDIRECT,
|
||||
onBeforeRedirect,
|
||||
new MutableRedirectUrlProvider(),
|
||||
onBeforeOAuthRedirect,
|
||||
);
|
||||
|
||||
const oauth = environment.oauth;
|
||||
expect(oauth?.navigation).toBeInstanceOf(BrowserNavigation);
|
||||
expect(oauth?.redirectUrlProvider).toBe(REDIRECT);
|
||||
expect(oauth?.storage).toBeDefined();
|
||||
if (!environment.oauth) {
|
||||
throw new Error("expected oauth config");
|
||||
}
|
||||
const { navigation } = environment.oauth;
|
||||
if (!navigation) {
|
||||
throw new Error("expected navigation");
|
||||
}
|
||||
const authUrl = new URL("https://idp.example/authorize?state=abc");
|
||||
navigation.navigateToAuthorization(authUrl);
|
||||
expect(onBeforeOAuthRedirect).toHaveBeenCalledWith(authUrl);
|
||||
});
|
||||
|
||||
it("works without an onBeforeOAuthRedirect callback", () => {
|
||||
const { environment } = createWebEnvironment(undefined, REDIRECT);
|
||||
expect(environment.oauth?.navigation).toBeInstanceOf(BrowserNavigation);
|
||||
it("routes fetch through the wrapped global fetch at the window origin", async () => {
|
||||
const remoteBody = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { "content-type": "text/plain" },
|
||||
body: "echoed",
|
||||
};
|
||||
const fetchMock = vi.fn<typeof fetch>().mockImplementation(
|
||||
async () =>
|
||||
new Response(JSON.stringify(remoteBody), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { environment } = createWebEnvironment(
|
||||
"api-tok",
|
||||
new MutableRedirectUrlProvider(),
|
||||
);
|
||||
fetchMock.mockClear();
|
||||
|
||||
const res = await environment.fetch!("http://upstream.test/mcp");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("echoed");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
|
||||
"http://127.0.0.1:6299/api/fetch",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "@inspector/core/mcp/remote/index.js";
|
||||
import { BrowserNavigation } from "@inspector/core/auth/browser/index.js";
|
||||
import type { RedirectUrlProvider } from "@inspector/core/auth/index.js";
|
||||
import { getRemoteOAuthStorage } from "./remoteOAuthStorage";
|
||||
import { getWebRemoteOAuthStorage } from "./remoteOAuthStorage.js";
|
||||
|
||||
export interface WebEnvironmentResult {
|
||||
environment: InspectorClientEnvironment;
|
||||
|
|
@ -18,19 +18,17 @@ export interface WebEnvironmentResult {
|
|||
* - transport / fetch / logger all routed through the in-process Hono
|
||||
* backend at `window.location.origin` (the `clients/web/server`
|
||||
* dev-backend wires this in `/api/*`).
|
||||
* - OAuth storage uses `RemoteOAuthStorage` (POSTs to the same backend's
|
||||
* `/api/storage/oauth`, which writes `~/.mcp-inspector/storage/oauth.json`
|
||||
* mode 0600), so a token obtained in this browser session is also
|
||||
* available to the CLI/TUI on the same host. Navigation still uses
|
||||
* `BrowserNavigation` (full-page redirect).
|
||||
* - OAuth storage uses `RemoteOAuthStorage` (shared `oauth.json` via
|
||||
* `/api/storage/oauth`); navigation uses `BrowserNavigation`.
|
||||
*
|
||||
* Returns both the assembled environment and the logger so callers can share
|
||||
* the same pino instance for any direct logging they need to do, instead of
|
||||
* reaching back through the client.
|
||||
*
|
||||
* `authToken` is read from a higher level (currently unused in this app since
|
||||
* v2 has no auth-token UI yet, but kept in the signature so the wiring is
|
||||
* ready when token plumbing lands).
|
||||
* `authToken` is supplied by `App.tsx` (`getAuthToken()`) and forwarded to
|
||||
* remote transport/fetch/logger and `getWebRemoteOAuthStorage` so every
|
||||
* `/api/*` call (including `/api/storage/oauth`) carries `x-mcp-remote-auth`
|
||||
* when the backend requires it.
|
||||
*
|
||||
* `onBeforeOAuthRedirect` runs synchronously immediately before the OAuth
|
||||
* full-page redirect (see `BrowserNavigation`). The app uses it to flush the
|
||||
|
|
@ -68,7 +66,7 @@ export function createWebEnvironment(
|
|||
}),
|
||||
logger,
|
||||
oauth: {
|
||||
storage: getRemoteOAuthStorage(baseUrl, authToken, fetchFn),
|
||||
storage: getWebRemoteOAuthStorage(authToken),
|
||||
navigation: new BrowserNavigation(undefined, onBeforeOAuthRedirect),
|
||||
redirectUrlProvider,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,64 +1,66 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/index.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getRemoteOAuthStorage,
|
||||
getWebOAuthBaseUrl,
|
||||
webOAuthFetch,
|
||||
getWebRemoteOAuthStorage,
|
||||
resetWebRemoteOAuthStorageCacheForTests,
|
||||
} from "./remoteOAuthStorage";
|
||||
|
||||
const NOOP_FETCH = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ state: { servers: {} }, version: 0 }), {
|
||||
status: 200,
|
||||
}),
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
describe("remoteOAuthStorage", () => {
|
||||
it("getWebOAuthBaseUrl derives the backend origin from window.location", () => {
|
||||
expect(getWebOAuthBaseUrl()).toBe(
|
||||
`${window.location.protocol}//${window.location.host}`,
|
||||
afterEach(() => {
|
||||
resetWebRemoteOAuthStorageCacheForTests();
|
||||
});
|
||||
|
||||
it("returns one RemoteOAuthStorage instance per cache key", () => {
|
||||
const a = getRemoteOAuthStorage({
|
||||
baseUrl: "http://127.0.0.1:6277",
|
||||
authToken: "tok-a",
|
||||
});
|
||||
const aAgain = getRemoteOAuthStorage({
|
||||
baseUrl: "http://127.0.0.1:6277",
|
||||
authToken: "tok-a",
|
||||
});
|
||||
const b = getRemoteOAuthStorage({
|
||||
baseUrl: "http://127.0.0.1:6277",
|
||||
authToken: "tok-b",
|
||||
});
|
||||
|
||||
expect(aAgain).toBe(a);
|
||||
expect(b).not.toBe(a);
|
||||
});
|
||||
|
||||
it("getWebRemoteOAuthStorage uses window.location origin", () => {
|
||||
vi.stubGlobal("window", {
|
||||
location: {
|
||||
protocol: "http:",
|
||||
host: "127.0.0.1:6299",
|
||||
},
|
||||
});
|
||||
|
||||
const storage = getWebRemoteOAuthStorage("smoke-web-token");
|
||||
const again = getWebRemoteOAuthStorage("smoke-web-token");
|
||||
|
||||
expect(again).toBe(storage);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("throws when window is unavailable", () => {
|
||||
vi.stubGlobal("window", undefined);
|
||||
expect(() => getWebRemoteOAuthStorage()).toThrow(
|
||||
"getWebRemoteOAuthStorage requires a browser environment",
|
||||
);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("returns a RemoteOAuthStorage instance", () => {
|
||||
const storage = getRemoteOAuthStorage(
|
||||
"http://a.example",
|
||||
undefined,
|
||||
NOOP_FETCH,
|
||||
);
|
||||
expect(storage).toBeInstanceOf(RemoteOAuthStorage);
|
||||
});
|
||||
|
||||
it("memoizes one instance per {baseUrl, authToken}", () => {
|
||||
const a1 = getRemoteOAuthStorage("http://memo.example", "tok", NOOP_FETCH);
|
||||
const a2 = getRemoteOAuthStorage("http://memo.example", "tok", NOOP_FETCH);
|
||||
expect(a1).toBe(a2);
|
||||
|
||||
// A different auth token is a distinct key → distinct instance.
|
||||
const b = getRemoteOAuthStorage("http://memo.example", "other", NOOP_FETCH);
|
||||
expect(b).not.toBe(a1);
|
||||
|
||||
// A different base URL is a distinct key → distinct instance.
|
||||
const c = getRemoteOAuthStorage("http://memo2.example", "tok", NOOP_FETCH);
|
||||
expect(c).not.toBe(a1);
|
||||
});
|
||||
|
||||
it("treats an undefined authToken as its own stable key", () => {
|
||||
const a = getRemoteOAuthStorage("http://undef.example", undefined);
|
||||
const b = getRemoteOAuthStorage("http://undef.example", undefined);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("webOAuthFetch delegates to globalThis.fetch preserving the receiver", async () => {
|
||||
const spy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
try {
|
||||
const res = await webOAuthFetch("http://x.example");
|
||||
expect(spy).toHaveBeenCalledWith("http://x.example");
|
||||
expect(res.status).toBe(200);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
it("creates a new instance after the test cache reset", () => {
|
||||
const first = getRemoteOAuthStorage({
|
||||
baseUrl: "http://127.0.0.1:6277",
|
||||
authToken: "reset-me",
|
||||
});
|
||||
resetWebRemoteOAuthStorageCacheForTests();
|
||||
const second = getRemoteOAuthStorage({
|
||||
baseUrl: "http://127.0.0.1:6277",
|
||||
authToken: "reset-me",
|
||||
});
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,56 +1,59 @@
|
|||
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/index.js";
|
||||
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/storage-remote.js";
|
||||
|
||||
/**
|
||||
* Shared `RemoteOAuthStorage` accessor for the web client.
|
||||
*
|
||||
* The browser's OAuth state is persisted through the in-process Hono backend
|
||||
* (`POST /api/storage/oauth` → `~/.mcp-inspector/storage/oauth.json`, mode
|
||||
* 0600) rather than `sessionStorage`, so a credential obtained in the browser
|
||||
* is the same blob the TUI/CLI read on the same host (web ⇄ TUI ⇄ CLI parity).
|
||||
*
|
||||
* One instance per `{baseUrl, authToken}`. The environment factory is called
|
||||
* per-connect (a fresh `InspectorClient` is built for each server switch), but
|
||||
* the OAuth blob on disk is a single shared resource: a stale instance's
|
||||
* whole-blob POST could clobber a write made by a newer one. Memoizing also
|
||||
* avoids re-hydrating the same file on every connect, and lets the connection
|
||||
* path, the EMA IdP hook, and the per-server "clear OAuth" action all share
|
||||
* one in-memory view of the store.
|
||||
*/
|
||||
const oauthStorageCache = new Map<string, RemoteOAuthStorage>();
|
||||
|
||||
/** Backend origin the web client talks to (same origin that serves the SPA). */
|
||||
export function getWebOAuthBaseUrl(): string {
|
||||
return `${window.location.protocol}//${window.location.host}`;
|
||||
export interface WebRemoteOAuthStorageOptions {
|
||||
baseUrl: string;
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `window.fetch` loses its `this` binding when extracted, raising "Illegal
|
||||
* invocation"; wrap so the call site preserves the global receiver.
|
||||
*/
|
||||
export const webOAuthFetch: typeof fetch = (...args) =>
|
||||
globalThis.fetch(...args);
|
||||
let cached: { cacheKey: string; storage: RemoteOAuthStorage } | undefined;
|
||||
|
||||
function buildCacheKey(options: WebRemoteOAuthStorageOptions): string {
|
||||
return `${options.baseUrl}\0${options.authToken ?? ""}`;
|
||||
}
|
||||
|
||||
const defaultFetch: typeof fetch = (...args) => globalThis.fetch(...args);
|
||||
|
||||
/**
|
||||
* Memoized `RemoteOAuthStorage` for the given backend + auth token.
|
||||
* Shared web OAuth store: `RemoteOAuthStorage` → `GET/POST /api/storage/oauth`
|
||||
* → `~/.mcp-inspector/storage/oauth.json` (same file as CLI/TUI).
|
||||
*
|
||||
* The cache key is intentionally `{baseUrl, authToken}` only — `fetchFn` is NOT
|
||||
* part of it. Whichever call constructs an instance first wins, so a later
|
||||
* caller's `fetchFn` is ignored. This is safe today because every call site
|
||||
* either omits `fetchFn` (defaulting to {@link webOAuthFetch}) or passes an
|
||||
* `environmentFactory` wrapper that is functionally identical (`globalThis.fetch`),
|
||||
* so the shared-instance goal (one whole-blob writer per backend) matters more
|
||||
* than which wrapper is used. A future caller needing a genuinely different
|
||||
* transport must key on it (or bypass the cache).
|
||||
* Caches a single instance keyed by `{ baseUrl, authToken }`: repeat calls with
|
||||
* the same key return that instance, so connect, EMA IdP session, and per-server
|
||||
* clear all mutate the same in-memory view. A call with a different key replaces
|
||||
* the cached instance (cache of one). That is sufficient because the web app
|
||||
* uses a stable origin + page-lifetime API token, so the key never changes
|
||||
* within a session.
|
||||
*/
|
||||
export function getRemoteOAuthStorage(
|
||||
baseUrl: string,
|
||||
authToken: string | undefined,
|
||||
fetchFn: typeof fetch = webOAuthFetch,
|
||||
options: WebRemoteOAuthStorageOptions,
|
||||
): RemoteOAuthStorage {
|
||||
const key = `${baseUrl} ${authToken ?? ""}`;
|
||||
const cached = oauthStorageCache.get(key);
|
||||
if (cached) return cached;
|
||||
const storage = new RemoteOAuthStorage({ baseUrl, authToken, fetchFn });
|
||||
oauthStorageCache.set(key, storage);
|
||||
return storage;
|
||||
const cacheKey = buildCacheKey(options);
|
||||
if (cached?.cacheKey === cacheKey) {
|
||||
return cached.storage;
|
||||
}
|
||||
cached = {
|
||||
cacheKey,
|
||||
storage: new RemoteOAuthStorage({
|
||||
baseUrl: options.baseUrl,
|
||||
authToken: options.authToken,
|
||||
fetchFn: defaultFetch,
|
||||
}),
|
||||
};
|
||||
return cached.storage;
|
||||
}
|
||||
|
||||
/** Current origin + optional API token (see `getAuthToken()` in App.tsx). */
|
||||
export function getWebRemoteOAuthStorage(
|
||||
authToken?: string,
|
||||
): RemoteOAuthStorage {
|
||||
if (typeof window === "undefined") {
|
||||
throw new Error("getWebRemoteOAuthStorage requires a browser environment");
|
||||
}
|
||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
return getRemoteOAuthStorage({ baseUrl, authToken });
|
||||
}
|
||||
|
||||
/** @internal Vitest isolation */
|
||||
export function resetWebRemoteOAuthStorageCacheForTests(): void {
|
||||
cached = undefined;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue