koala73/worldmonitor · warning

MCP_INTERNAL_HMAC_SECRET not configured

Error message

MCP_INTERNAL_HMAC_SECRET not configured

What it means

requireCurrentConvexUser(userId, action) backs every account-bound billing operation: it calls waitForConvexAuthForUser(userId) and throws `Account changed while ${action}. Try again.` when the shared ConvexClient's auth no longer belongs to the initiating user (different barrier user, Clerk user changed, superseded setAuth generation, or the 10s token wait expired); assertAccountStillCurrent then re-verifies after success. This stops one account from claiming or mutating another's billing state.

Solutions

  1. Retry the billing action once sign-in settles; the operation is idempotent from the caller's perspective
  2. Keep the same account signed in across tabs for the duration of the operation
  3. Check console logs for repeated rebind/authGeneration churn if it persists
  4. For activation claims: a failed claim does not consume the one-time presentation; the claimNonce can be retried

Example fix

// before
await requireCurrentConvexUser(userId, 'claiming Pro activation'); // throws 'Account changed...'

// after: capture identity, verify stability, retry once
const userId = getCurrentClerkUser()?.id;
if (!userId) { openSignIn(); return; }
try {
  const status = await claimProActivationPresentation(activationKey, claimNonce);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Account changed') && getCurrentClerkUser()?.id === userId) {
    await claimProActivationPresentation(activationKey, claimNonce);
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

const userId = getCurrentClerkUser()?.id;
if (!userId) { openSignIn(); return; }
if (getCurrentClerkUser()?.id !== userId) { abortStaleOperation(); return; }
await claimProActivationPresentation(activationKey, claimNonce);

Type guard

const isAccountChangedError = (e: unknown): e is Error =>
  e instanceof Error && e.message.startsWith('Account changed');

Try / catch

try {
  await claimProActivationPresentation(activationKey, claimNonce);
} catch (e) {
  if (isAccountChangedError(e) && getCurrentClerkUser()?.id === userId) {
    await new Promise(r => setTimeout(r, 1000));
    await claimProActivationPresentation(activationKey, claimNonce);
  } else throw e;
}

Prevention

When it happens

Trigger: Sign-out or account switch mid-billing-operation; a concurrent auth rebind superseding the barrier; token propagation slower than the 10s timeout during operations like claiming Pro activation.

Common situations: Users switching accounts in another tab; token refresh storms after sign-in; slow networks stretching the barrier wait.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21). Data as JSON: /api/errors/d2407102fef41f8e. Report an issue: GitHub.

Appendix: source

Thrown at api/mcp/auth.ts:146

  if (!url || !token) return null;
  mcpAnonRatelimit = new Ratelimit({
    redis: new Redis({ url, token, retry: false }),
    limiter: Ratelimit.slidingWindow(60, '60 s'),
    prefix: 'rl:mcp:anon',
    analytics: false,
  });
  return mcpAnonRatelimit;
}

/**
 * Build the Authorization header set for a downstream `_execute` fetch.
 *
 *   - env_key → `X-WorldMonitor-Key: <apiKey>` (legacy operator keys).
 *   - pro / user_key → `X-WM-MCP-Internal: <ts>.<sig>` + `X-WM-MCP-User-Id`.
 *               Signature binds method+pathname+queryHash+bodyHash+userId.
 *               Identity-resolved dashboard keys use this path so the
 *               gateway does not increment the shared daily account meter
 *               a second time after MCP already reserved the tool weight.
 *
 * `body` MUST be the EXACT bytes the caller passes to `fetch()` so the
 * signed payload matches the wire bytes. For JSON, pre-stringify on the
 * caller side and pass the same string here.
 */
export async function buildAuthHeaders(
  context: McpAuthContext,
  method: string,
  url: string,
  body: BodyInit | null | undefined,
): Promise<Record<string, string>> {
  if (context.kind === 'env_key') {
    return { 'X-WorldMonitor-Key': context.apiKey };
  }
  if (context.kind === 'free') {
    // U7: a free-tier context has no principal to authenticate as, so there is
    // nothing honest to sign. Throwing is the fail-closed choice — the
    // alternative (falling through to the HMAC branch below) would mint

View on GitHub (pinned to 7d06c8633d)