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.

Source

Thrown at api/mcp/auth.ts:145

    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 `pro` HMAC branch below) would mint
    // an internally-trusted signature for an anonymous caller, which is the
    // one outcome the free tier must never produce. A free-tier tool that
    // reaches here is misconfigured: it declared `_freeTier` while calling a
    // credentialed downstream.
    throw new Error('buildAuthHeaders: free-tier context has no credentials — a free-tier tool must not call a credentialed downstream');
  }
  // context.kind === 'pro'
  const secret = process.env.MCP_INTERNAL_HMAC_SECRET ?? '';
  if (!secret) {
    // Should never happen in production (deploy gate at U10) — surface as
    // an error so the tool fetch fails fast rather than silently 401-ing
    // at the gateway with a confusing "invalid_internal_mcp_signature".
    throw new Error('MCP_INTERNAL_HMAC_SECRET not configured');
  }
  const signed = await signInternalMcpRequest({
    method,
    url,
    body,
    userId: context.userId,
    secret,
  });
  return buildInternalMcpHeaders(signed);
}

export const PRODUCTION_DEPS: McpHandlerDeps = {
  resolveBearerToContext,
  // Preserve the validator's revoked/transient distinction: revoked grants
  // are 401 invalid_token, while a Convex/network outage is a retryable 503.
  validateProMcpToken,
  getEntitlements,
  validateUserApiKey,

View on GitHub (pinned to a96956387a)

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@a96956387a (2026-08-21). Data as JSON: /api/errors/d2407102fef41f8e. Report an issue: GitHub.