Skip to content

fix(openai-middleware): prevent double-wrapping and cross-tenant memory leakage on shared client#993

Open
devteamaegis wants to merge 2 commits into
supermemoryai:mainfrom
devteamaegis:fix/openai-middleware-double-wrap
Open

fix(openai-middleware): prevent double-wrapping and cross-tenant memory leakage on shared client#993
devteamaegis wants to merge 2 commits into
supermemoryai:mainfrom
devteamaegis:fix/openai-middleware-double-wrap

Conversation

@devteamaegis
Copy link
Copy Markdown

Problem

createOpenAIMiddleware / withSupermemory mutates the caller's OpenAI client in-place by overwriting openaiClient.chat.completions.create. In the most common server-side usage pattern — one shared OpenAI instance, withSupermemory called per-request with the current user's containerTag — this creates two bugs:

1. Double memory injection (latency / cost)

Every call after the first captures the already-wrapped create as originalCreate, stacking wrappers:

request 1  → user-A wrapper installed
request 2  → user-B wrapper installed ON TOP of user-A wrapper
             originalCreate (for user-B) = user-A's createWithMemory

completion → user-B injects memories → calls user-A wrapper → injects again → real API call

Each completion now hits the Supermemory profile API twice, doubling latency and token cost.

2. Cross-tenant memory leakage (security)

The inner wrapper (user-A's) runs with containerTag = "user-A", so user-B's completion receives user-A's memories injected into its system prompt.

// Server handler — shared client
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

app.post("/chat", async (req, res) => {
  const client = withSupermemory(openai, {  // ← BUG: mutates shared `openai`
    containerTag: req.user.id,
    customId: req.body.conversationId,
  })
  // After the second request, `openai.chat.completions.create` is a
  // double-wrapped function. Every completion includes memories from
  // both the current user AND the previous request's user.
  const response = await client.chat.completions.create(...)
})

Fix

Attach the original SDK method to every installed wrapper via a private Symbol (ORIGINAL_CREATE_SYM). Before capturing originalCreate, unwrap through the symbol if present. This ensures no matter how many times createOpenAIMiddleware is called on the same client, the innermost call is always the real SDK create:

const ORIGINAL_CREATE_SYM = Symbol("supermemory.originalCreate")

// Capture (unwrap if previously wrapped):
const currentCreate = openaiClient.chat.completions.create
const originalCreate = currentCreate[ORIGINAL_CREATE_SYM] ?? currentCreate

// After building createWithMemory, stamp the original on it:
createWithMemory[ORIGINAL_CREATE_SYM] = originalCreate

// Then install as before:
openaiClient.chat.completions.create = createWithMemory

The same pattern is applied to the Responses API path.

Tests

Three unit tests added (middleware.test.ts, no network calls — fetch is stubbed):

Test Asserts
Two calls → real create invoked once No double-wrapping
Three calls → real create invoked once Idempotent for N calls
Latest call wins containerTag from second call used, not first
✓ calling createOpenAIMiddleware twice ... invokes the real SDK create once
✓ calling createOpenAIMiddleware three times ... invokes the real SDK create once  
✓ the wrapper installed by the second call replaces the first (latest wins)

Notes

  • The public API surface is unchanged — withSupermemory still returns the same mutated client.
  • A longer-term fix would be to return a Proxy instead of mutating the input, but that's a breaking change. This Symbol guard is backward-compatible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant