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
- Retry the billing action once sign-in settles; the operation is idempotent from the caller's perspective
- Keep the same account signed in across tabs for the duration of the operation
- Check console logs for repeated rebind/authGeneration churn if it persists
- 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
- Capture the initiating userId before any await; never trust a plain auth-wait across account switches
- Failed claims do not consume the one-time presentation; retry with the same claimNonce
- Keep sign-in stable across tabs for the duration of billing operations
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
- HTTP ${response.status}
- PRO_REQUIRED
- UNAUTHENTICATED
- Authentication unavailable while loading MCP clients. Try ag
- Authenticated account changed during push setup
AI-assisted analysis of koala73/worldmonitor@a96956387a (2026-08-21).
Data as JSON: /api/errors/d2407102fef41f8e.
Report an issue: GitHub.