Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage.

This commit is contained in:
Bob Dickinson 2026-07-04 21:56:45 -07:00
commit 3c65bdb4c6
4 changed files with 863 additions and 1008 deletions

View file

@ -1,32 +0,0 @@
/**
* File-based storage adapter for Zustand persist middleware.
* Stores entire store state as JSON in a single file using atomic I/O.
*/
import { createJSONStorage } from "zustand/middleware";
import { readStoreFile, writeStoreFile, deleteStoreFile } from "../store-io.js";
export interface FileStorageAdapterOptions {
/** Full path to the storage file */
filePath: string;
}
/**
* Creates a Zustand storage adapter that reads/writes from a file.
* Conforms to Zustand's StateStorage interface.
*/
export function createFileStorageAdapter(
options: FileStorageAdapterOptions,
): ReturnType<typeof createJSONStorage> {
return createJSONStorage(() => ({
getItem: async () => readStoreFile(options.filePath),
// Do not introduce an `await` before writeStoreFile() here: it registers
// the write in pendingWrites synchronously, which is load-bearing for
// flushStoreFileWrites() (a microtask hop before registration would make a
// flush called right after setItem find an empty map and return early,
// silently regressing tests to non-deterministic).
setItem: async (_name: string, value: string) =>
writeStoreFile(options.filePath, value),
removeItem: async () => deleteStoreFile(options.filePath),
}));
}

View file

@ -1,10 +0,0 @@
/**
* Storage adapters for Zustand persist middleware.
* Provides adapters for file, remote HTTP, and browser storage.
*/
export { createFileStorageAdapter } from "./file-storage.js";
export type { FileStorageAdapterOptions } from "./file-storage.js";
export { createRemoteStorageAdapter } from "./remote-storage.js";
export type { RemoteStorageAdapterOptions } from "./remote-storage.js";

View file

@ -1,94 +0,0 @@
/**
* Remote HTTP storage adapter for Zustand persist middleware.
* Stores entire store state via HTTP API (GET/POST/DELETE /api/storage/:storeId).
*/
import { createJSONStorage } from "zustand/middleware";
export interface RemoteStorageAdapterOptions {
/** Base URL of the remote server (e.g. http://localhost:3000) */
baseUrl: string;
/** Store ID (e.g. "oauth", "inspector-settings") */
storeId: string;
/** Optional auth token for x-mcp-remote-auth header */
authToken?: string;
/** Fetch function to use (default: globalThis.fetch) */
fetchFn?: typeof fetch;
}
/**
* Creates a Zustand storage adapter that reads/writes via HTTP API.
* Conforms to Zustand's StateStorage interface.
*/
export function createRemoteStorageAdapter(
options: RemoteStorageAdapterOptions,
): ReturnType<typeof createJSONStorage> {
const baseUrl = options.baseUrl.replace(/\/$/, "");
const fetchFn = options.fetchFn ?? globalThis.fetch;
return createJSONStorage(() => ({
getItem: async (_name: string) => {
const headers: Record<string, string> = {};
if (options.authToken) {
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
}
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
method: "GET",
headers,
});
if (!res.ok) {
if (res.status === 404) {
return null;
}
throw new Error(`Failed to read store: ${res.status}`);
}
const store = await res.json();
// Zustand stores: { state: {...}, version: number }
// API returns the stored blob. If empty, Zustand hasn't initialized yet.
if (Object.keys(store).length === 0) {
return null; // Empty store means not initialized yet
}
// Return the stored Zustand format as string
return JSON.stringify(store);
},
setItem: async (_name: string, value: string) => {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (options.authToken) {
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
}
// Zustand gives us the full persisted format as a string
// Store it as-is (the API treats it as an opaque blob)
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
method: "POST",
headers,
body: value, // Already a JSON string from Zustand
});
if (!res.ok) {
throw new Error(`Failed to write store: ${res.status}`);
}
},
removeItem: async (_name: string) => {
const headers: Record<string, string> = {};
if (options.authToken) {
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
}
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
method: "DELETE",
headers,
});
// 404 is fine (already deleted), but other errors should propagate
if (!res.ok && res.status !== 404) {
throw new Error(`Failed to delete store: ${res.status}`);
}
},
}));
}

View file

@ -1,6 +1,6 @@
/**
* Shared storage path resolution, validation, and atomic file I/O.
* Used by the file storage adapter and the remote server's /api/storage routes.
* Used by OAuth file persistence and the remote server's /api/storage routes.
*/
import * as path from "node:path";
@ -57,15 +57,7 @@ export async function readStoreFile(filePath: string): Promise<string | null> {
/**
* In-flight writeStoreFile() promises, keyed by resolved path. Lets callers
* await persistence completion via flushStoreFileWrites() instead of polling
* the file — Zustand's persist middleware invokes writeStoreFile() fire-and-
* forget, so the in-memory store updates synchronously while the file write
* lags. Entries are removed once their write settles.
*
* Load-bearing: pendingWrites.set() below runs synchronously before the first
* await in writeStoreFile(), so a flushStoreFileWrites() called right after a
* persist sees the in-flight entry. Callers (e.g. the storage adapter's
* setItem) must not introduce an await before writeStoreFile() — doing so would
* let a flush run before registration and return early.
* the file. Entries are removed once their write settles.
*/
const pendingWrites = new Map<string, Promise<void>>();
@ -104,9 +96,8 @@ export async function writeStoreFile(
/**
* Await pending writeStoreFile() writes — those for `filePath` if given, else
* all of them. Use in tests after triggering persistence (Zustand persist
* writes fire-and-forget) instead of polling the file, and for graceful
* shutdown. Resolves immediately when nothing is in flight.
* all of them. Use in tests after triggering persistence instead of polling
* the file, and for graceful shutdown. Resolves immediately when nothing is in flight.
*/
export async function flushStoreFileWrites(filePath?: string): Promise<void> {
if (filePath !== undefined) {