MiniSearch Verification Token System: Secure Search with verifiedTokens and handleTokenVerification

MiniSearch protects its search endpoint using an Argon2-based verification token system that caches successful authentications in an in-memory verifiedTokens Set and handles HTTP responses through handleTokenVerification to enforce rate limits and prevent brute-force attacks.

The felladrin/minisearch repository implements a lightweight authentication layer to protect its search API from unauthorized access and abuse. The verification token system combines cryptographic token validation using Argon2 with in-memory session caching and strict per-token rate limiting to balance security with performance. This architecture stores secrets in temporary files, caches successful verifications in a process-level Set, and uses dedicated helper functions to standardize HTTP responses across the application.

Token Generation and Secret Storage

MiniSearch lazily generates a cryptographically random token on first access and persists it to a temporary file for subsequent server restarts. In server/searchToken.ts, the system checks for existing tokens at temp-dir/minisearch-token before generating new secrets, ensuring persistence across process lifecycles without requiring external databases.

// server/searchToken.ts
import { regenerateSearchToken, getSearchToken } from "./server/searchToken";

// Generate a new secret token (e.g., after security rotation)
regenerateSearchToken();
console.log("Token stored at temp-dir/minisearch-token");

// Read the current token for client requests
const token = getSearchToken();

The token file lives in the operating system's temporary directory, making it suitable for containerized deployments where filesystem persistence is acceptable but database dependencies are undesirable.

Core Verification Logic in verifyTokenAndRateLimit

The verifyTokenAndRateLimit() function in server/verifyTokenAndRateLimit.ts serves as the primary gatekeeper for all protected requests. This async function validates incoming tokens against the stored secret using Argon2 hashing from hash-wasm, checks the in-memory verifiedTokens cache to avoid redundant cryptographic operations, and enforces consumption-based rate limiting.

Argon2 Hash Verification

When a token arrives via the Authorization: Bearer <token> header, the system first checks the verifiedTokens Set for existing validation. If absent, it compares the submitted token against the stored secret using Argon2id, adding the token to the cache only upon successful verification.

// Conceptual flow from server/verifyTokenAndRateLimit.ts
const verifiedTokens = new Set<string>();

async function verifyTokenAndRateLimit(token: string | null) {
  if (!token) return { isAuthorized: false, statusCode: 400, error: "Missing token." };
  
  // Check in-memory cache first
  if (verifiedTokens.has(token)) {
    // Skip expensive Argon2 verification
    return checkRateLimit(token);
  }
  
  // Verify using Argon2 if not cached
  const isValid = await verifyArgon2(token, storedSecret);
  if (!isValid) return { isAuthorized: false, statusCode: 401, error: "Invalid token." };
  
  verifiedTokens.add(token); // Cache for subsequent requests
  return checkRateLimit(token);
}

Per-Token Rate Limiting

After cryptographic verification, the system applies rate limiting using RateLimiterMemory from rate-limiter-flexible. Each token receives a bucket of 10 requests per 10-second window. Successful verification consumes one point; exhausted buckets return 429 Too Many Requests.

import { RateLimiterMemory } from "rate-limiter-flexible";

const rateLimiter = new RateLimiterMemory({
  points: 10,
  duration: 10, // seconds
});

This configuration prevents brute-force attacks against the Argon2 hash while ensuring legitimate clients can perform burst searches without unnecessary latency.

In-Memory Token Caching via verifiedTokens

The server/verifiedTokens.ts module exports a process-level Set<string> that maintains the list of tokens that have successfully passed Argon2 verification during the current server session. This cache eliminates the computational overhead of repeated hash comparisons for active clients.

// server/verifiedTokens.ts
const verifiedTokens = new Set<string>();

export function getVerifiedTokensAmount() { 
  return verifiedTokens.size; 
}

export function isVerifiedToken(token: string) { 
  return verifiedTokens.has(token); 
}

export function addVerifiedToken(token: string) { 
  return verifiedTokens.add(token); 
}

Important: This Set exists only in memory and does not persist across process restarts. When the Node.js process terminates or restarts, the verifiedTokens collection initializes empty, requiring clients to re-verify against the Argon2 hash on their next request.

HTTP Response Standardization with handleTokenVerification

The handleTokenVerification() function in server/handleTokenVerification.ts provides a thin HTTP layer that translates verification results into proper JSON responses. This utility ensures consistent error formatting and status codes across all protected endpoints.

// server/handleTokenVerification.ts
export async function handleTokenVerification(token: string | null, response: ServerResponse) {
  const { isAuthorized, statusCode, error } = await verifyTokenAndRateLimit(token);
  
  if (!isAuthorized && statusCode && error) {
    response.statusCode = statusCode; // 400, 401, or 429
    response.setHeader("Content-Type", "application/json");
    response.end(JSON.stringify({ error }));
    return { shouldContinue: false };
  }
  
  return { shouldContinue: true };
}

When integrated into an HTTP server, this function returns { shouldContinue: false } to signal that the response has already been sent and request processing should halt. Valid tokens return { shouldContinue: true }, allowing the endpoint to execute the actual search logic.

Wiring the Verification Layer

import { createServer } from "node:http";
import { handleTokenVerification } from "./server/handleTokenVerification";

const server = createServer(async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer\s+/, "") ?? null;
  
  const { shouldContinue } = await handleTokenVerification(token, res);
  if (!shouldContinue) return; // Error response already sent
  
  // Proceed with search implementation
  res.end(JSON.stringify({ results: [] }));
});

Summary

  • MiniSearch stores secrets lazily in temp-dir/minisearch-token and reads them via getSearchToken() from server/searchToken.ts.
  • Argon2 verification occurs only for uncached tokens; successful validations are stored in the in-memory verifiedTokens Set to optimize performance.
  • Rate limiting enforces 10 requests per 10 seconds per token using RateLimiterMemory, returning HTTP 429 when limits are exceeded.
  • handleTokenVerification() standardizes HTTP responses, returning { shouldContinue: false } to halt processing on authentication failures with appropriate status codes (400, 401, 429).

Frequently Asked Questions

How does MiniSearch store the secret verification token?

The system lazily generates a random token on first access and persists it to a temporary file at temp-dir/minisearch-token within the operating system's temp directory. The regenerateSearchToken() function in server/searchToken.ts handles creation, while getSearchToken() reads the existing token from disk on subsequent calls.

What happens to verified tokens when the server restarts?

The verifiedTokens Set is stored only in memory and does not persist across process restarts. When the Node.js process terminates, the cache clears completely. Clients must re-verify their tokens using Argon2 comparison on the first request after a server restart, after which the tokens are re-added to the fresh in-memory cache.

How does the rate limiting work per token?

Each token receives an independent rate limit bucket via RateLimiterMemory configured with points: 10 and duration: 10. Every successful verification consumes one point from the bucket. When a token exhausts its 10 requests within the 10-second window, verifyTokenAndRateLimit() returns a 429 status code with the error message "Too many requests."

Why does the system use Argon2 instead of simple string comparison?

According to the felladrin/minisearch source code, the system uses Argon2id hashing via hash-wasm to prevent timing attacks and protect against brute-force attempts if the temporary token file is compromised. While the verifiedTokens cache provides fast subsequent lookups, the initial verification uses slow cryptographic hashing to mitigate security risks inherent in storing secrets on disk.

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 →