#1758·corsair

Multi-Tenant `singleFlight` Concurrency Bypass & OAuth Token Invalidation

Author: codeCraft-RitikCreated Sep 21, 2026Updated Sep 21, 2026
Labelsbug

Steps to reproduce

1. Multi-Tenant singleFlight Concurrency Bypass & OAuth Token Invalidation

️ Overview

Expected Behavior

Root Cause Analysis

In Corsair, token refresh deduplication is managed by singleFlight() in core/auth/single-flight.ts:

typescript
// packages/corsair/core/auth/single-flight.ts
const flightsByStore = new WeakMap<object, Map<string, Promise<unknown>>>();

export function singleFlight<T>(
    store: object,
    key: string,
    run: () => Promise<T>,
): Promise<T> {
    let flights = flightsByStore.get(store);
    if (!flights) {
        flights = new Map();
        flightsByStore.set(store, flights);
    }
    const existing = flights.get(key) as Promise<T> | undefined;
    if (existing !== undefined) return existing;
    const pending = run().finally(() => {
        flights.delete(key);
    });
    flights.set(key, pending);
    return pending;
}

In core/auth/oauth-access.ts, singleFlight is invoked passing ctx.keys as the store argument:

typescript
// packages/corsair/core/auth/oauth-access.ts:85-87
const runRefresh = (force: boolean): Promise<string> =>
    singleFlight(ctx.keys, flightKey, async () => { ... });

However, in multi-tenant mode (multiTenancy: true), every call to corsair.withTenant(tenantId) generates a brand-new client instance via buildCorsairClient():

typescript
// packages/corsair/core/index.ts:117-138
if (config.multiTenancy) {
    const tenantWrapper = Object.assign(
        {
            withTenant: (tenantId: string) => {
                const client = buildCorsairClient(config.plugins, {
                    database: resolvedDatabase,
                    tenantId,
                    ...
                });
                return client;
            },
            ...
        }
    );
    return tenantWrapper;
}

Inside buildCorsairClient(), a new accountKeyManager object is instantiated on every single call:

typescript
// packages/corsair/core/client/index.ts:470-479
accountKeyManager = createAccountKeyManager({
    authType: pluginOptions.authType,
    integrationName: plugin.id,
    tenantId: effectiveTenantId,
    kek,
    database,
    extraAccountFields,
    ensureProvisioned,
});
apiUnsafe[plugin.id]!.keys = accountKeyManager;

Because WeakMap compares keys by reference identity (===), every invocation of corsair.withTenant('tenant-123') receives a distinct object reference for ctx.keys.

Corsair version

Release Version: [email protected] (declared in packages/corsair/package.json )

Environment details

bash
Runtime: Node.js (v18, v20, v22, v24) or Bun.
Operating System: OS-independent (Linux, macOS, Windows).
Execution Context:
Multi-tenant backend servers (Next.js App Router route handlers, Express, Fastify, Hono, NestJS).
Serverless or containerized environments receiving parallel HTTP requests for the same tenant.
AI agent workflows where an agent invokes multiple asynchronous tools in parallel (Promise.all([...]) or subagents).
Storage Layer: Any persistent database supported by Corsair (PostgreSQL, SQLite, MySQL via Kysely) storing OAuth tokens on corsair_accounts

What part of Corsair is affected?

Other

Configuration (if relevant)

typescript

Anything else

No response