How Minisearch Implements Search Token Hash Authentication

Minisearch secures its search API using an Argon2id-based search token hash system where the client derives a proof-of-knowledge hash from a build-time secret, caches it in localStorage to avoid repeated computation, and transmits it via the Authorization header for server-side verification against a filesystem-stored secret.

Minisearch employs a lightweight yet robust authentication mechanism to protect its search endpoints without exposing sensitive credentials over the wire. This search token hash system combines client-side Argon2id hashing, local caching strategies, and server-side verification with rate limiting to balance security with performance. The implementation spans both the client and server codebase, utilizing specific utilities in client/modules/searchTokenHash.ts and server/verifyTokenAndRateLimit.ts to create a complete authentication flow.

Server-Side Secret Generation

The authentication flow begins with a server-side secret that never leaves the filesystem. In server/searchToken.ts, Minisearch stores a random token in a temporary file named minisearch-token. If this file does not exist when the server starts, the system automatically generates a new secret.

const getSearchTokenFilePath = () => path.resolve(temporaryDirectory, "minisearch-token");

export const getSearchToken = () => {
  if (!existsSync(getSearchTokenFilePath())) regenerateSearchToken();
  return readFileSync(getSearchTokenFilePath(), "utf8");
};

export function regenerateSearchToken() {
  const newToken = Math.random().toString(36).substring(2);
  writeFileSync(getSearchTokenFilePath(), newToken);
}

This secret serves as the password input for all Argon2 operations. Because the token lives only in the server's temporary directory and is never transmitted to clients, the system maintains a strict separation between the secret material and the authentication tokens used in API requests.

Client-Side Hash Generation

The client derives the search token hash using Argon2id, a memory-hard hashing algorithm resistant to GPU attacks. Located in client/modules/searchTokenHash.ts, the generation logic uses the VITE_SEARCH_TOKEN environment variable (bundled at build time) as the password input.

const password = VITE_SEARCH_TOKEN;
const salt = new Uint8Array(16);
crypto.getRandomValues(salt);

const newSearchTokenHash = await argon2id({
  password,
  salt,
  parallelism: 1,
  iterations: 16,
  memorySize: 512,
  hashLength: 8,
  outputType: "encoded",
});

The implementation deliberately uses a short 8-byte hash length because the value functions solely as a proof-of-knowledge token rather than a cryptographic key. The Argon2id parameters (16 iterations, 512 KiB memory, single parallelism thread) provide sufficient work-factor protection while remaining computationally feasible for browser environments.

LocalStorage Caching Strategy

To prevent the client from re-running the expensive Argon2 computation on every search request, Minisearch caches the generated hash in the browser's localStorage. The caching mechanism resides in client/modules/pubSub.ts (lines 42-55) and is accessed through the lastSearchTokenHash PubSub wrapper.

Before generating a new hash, the client attempts to reuse the cached value:

const lastSearchTokenHash = getLastSearchTokenHash();
const lastSearchTokenHashIsValid = await argon2Verify({
  password,
  hash: lastSearchTokenHash,
});

if (lastSearchTokenHashIsValid) return lastSearchTokenHash;

// Otherwise generate fresh hash and cache it
updateLastSearchTokenHash(newSearchTokenHash);

This verification step ensures that cached hashes remain valid against the current secret. If the server regenerates its secret token, the cached hash fails verification, triggering the client to compute a new hash automatically.

Server Verification and Rate Limiting

When a request arrives at the search endpoint, the server extracts the token from the Authorization header and validates it through the pipeline defined in server/verifyTokenAndRateLimit.ts. The verification process employs a two-tier caching strategy to optimize performance.

First, the server checks an in-memory Set of verified tokens managed by server/verifiedTokens.ts:

if (!isVerifiedToken(token)) {
  const isValidToken = await argon2Verify({ 
    password: getSearchToken(), 
    hash: token 
  });
  
  if (!isValidToken) {
    return { isAuthorized: false, statusCode: 401, error: "Invalid token." };
  }
  
  addVerifiedToken(token); // Cache successful verification
}

Once verified, the token enters the verifiedTokens Set, allowing subsequent requests to skip the computationally expensive Argon2 verification. Finally, a RateLimiterMemory instance enforces a quota of 10 requests per 10 seconds per token:

await rateLimiter.consume(token);

This rate limiting occurs after authentication to prevent brute-force attacks against the Argon2 verification endpoint itself while allowing legitimate users to maintain cached verification status.

Request Handling Integration

The server/handleTokenVerification.ts module provides a thin HTTP-layer wrapper that translates verification results into proper JSON responses. Search endpoints utilize this handler to enforce authentication before executing queries:

const token = req.headers["authorization"]?.toString().split(" ")[1] ?? null;
const { isAuthorized, statusCode, error } = await verifyTokenAndRateLimit(token);

if (!isAuthorized) {
  res.writeHead(statusCode, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ error }));
  return;
}

This separation of concerns allows the core verification logic to remain framework-agnostic while providing a standard interface for HTTP response generation.

Practical Implementation Examples

Client-Side API Call with Automatic Hash Retrieval

When initiating a search from the client, the application retrieves the cached or newly generated hash and attaches it to the request headers:

import { getSearchTokenHash } from "./searchTokenHash";

export async function callSearchApi(query: string) {
  const hash = await getSearchTokenHash();
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    headers: { Authorization: `Bearer ${hash}` },
  });
  return response.json();
}

Server-Side Endpoint Protection

API routes integrate token verification through the dedicated handler:

import { handleTokenVerification } from "./handleTokenVerification";
import type { IncomingMessage, ServerResponse } from "http";

export async function searchEndpoint(req: IncomingMessage, res: ServerResponse) {
  const token = req.headers["authorization"]?.toString().split(" ")[1] ?? null;
  const { shouldContinue } = await handleTokenVerification(token, res);
  
  if (!shouldContinue) return;
  
  // Proceed with search implementation
}

Manual Token Regeneration

For development or testing scenarios, you can force a new server secret:

import { regenerateSearchToken } from "./searchToken";

regenerateSearchToken(); // Creates new minisearch-token file

Summary

  • Server-side secret: Stored in a temporary file (minisearch-token) and never transmitted to clients.
  • Argon2id hashing: Client derives 8-byte proof-of-knowledge hashes using memory-hard parameters (16 iterations, 512 KiB).
  • Dual caching: Client caches hashes in localStorage; server maintains a Set of verified tokens to skip repeated Argon2 computations.
  • Rate limiting: Each token is restricted to 10 requests per 10 seconds via RateLimiterMemory.
  • Zero-trust verification: Every request validates the hash against the server secret unless previously verified in the current session.

Frequently Asked Questions

What hashing algorithm does Minisearch use for search token authentication?

Minisearch uses Argon2id, the recommended variant of the Argon2 password-hashing function that provides resistance against both GPU cracking attacks and side-channel timing attacks. The implementation in client/modules/searchTokenHash.ts configures Argon2id with 16 iterations, 512 KiB of memory, and single-threaded parallelism to balance security with browser performance.

How does the client avoid re-computing the hash on every search request?

The client caches the derived Argon2 hash in the browser's localStorage through the PubSub wrapper defined in client/modules/pubSub.ts. Before generating a new hash, the system verifies the cached hash against the secret using argon2Verify. If valid, it returns the cached value immediately, avoiding the computational cost of re-hashing until the server secret changes or the cache is cleared.

What happens when the server regenerates its secret token?

If the server calls regenerateSearchToken() or the minisearch-token file is deleted, the client-side cached hashes will fail verification against the new secret. The next search request will trigger the argon2Verify check in searchTokenHash.ts, detect the invalid cache, and automatically generate a new hash using the updated VITE_SEARCH_TOKEN value, seamlessly updating the localStorage cache with the valid credentials.

How does the rate limiting mechanism protect the search API?

The server implements per-token rate limiting using RateLimiterMemory configured to allow 10 requests per 10 seconds per unique hash token. This limit is enforced in server/verifyTokenAndRateLimit.ts after successful authentication, preventing abuse of the search endpoint while allowing legitimate users with cached verification status to make repeated queries without re-triggering the Argon2 verification overhead.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →