{"record":{"id":"6fc8295db091139a","repo":"koala73/worldmonitor","slug":"mcp-pro-grant-hmac-secret-is-not-set","errorCode":null,"errorMessage":"MCP_PRO_GRANT_HMAC_SECRET is not set","messagePattern":"MCP_PRO_GRANT_HMAC_SECRET is not set","errorType":"exception","errorClass":"GrantConfigError","httpStatus":null,"severity":"critical","filePath":"api/_mcp-grant-hmac.ts","lineNumber":87,"sourceCode":"\nasync function importHmacKey(secret: string): Promise<CryptoKey> {\n  return crypto.subtle.importKey(\n    'raw',\n    ENC.encode(secret),\n    { name: 'HMAC', hash: 'SHA-256' },\n    false,\n    ['sign', 'verify'],\n  );\n}\n\n/** Reads the env var. Throws GrantConfigError if missing/empty. */\nexport function readGrantSecret(env: NodeJS.ProcessEnv = process.env): string {\n  const secret = env.MCP_PRO_GRANT_HMAC_SECRET ?? '';\n  if (!secret) {\n    // Operator-visible and value-free; the PR #3646 MCP_* env-name inventory\n    // that used to sit here served its diagnostic purpose and is gone (#7278).\n    console.warn('[mcp-grant-hmac] MCP_PRO_GRANT_HMAC_SECRET is not set');\n    throw new GrantConfigError('MCP_PRO_GRANT_HMAC_SECRET is not set');\n  }\n  return secret;\n}\n\n/**\n * Sign a grant payload. Returns the wire-format token\n * `<base64url(payloadJson)>.<base64url(sig)>`.\n *\n * Deterministic for a given (payload, secret) pair: stringifies once,\n * signs the exact bytes, encodes both halves with base64url-no-pad.\n */\nexport async function signGrant(payload: GrantPayload, secret?: string): Promise<string> {\n  const sec = secret ?? readGrantSecret();\n  const json = JSON.stringify({ userId: payload.userId, nonce: payload.nonce, exp: payload.exp });\n  const payloadBytes = ENC.encode(json);\n  const key = await importHmacKey(sec);\n  const sig = new Uint8Array(await crypto.subtle.sign('HMAC', key, payloadBytes));\n  return `${base64UrlEncode(payloadBytes)}.${base64UrlEncode(sig)}`;","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/api/_mcp-grant-hmac.ts#L69-L105","documentation":"readGrantSecret() loads the HMAC secret used to sign and verify MCP pro grant tokens from the environment. If MCP_PRO_GRANT_HMAC_SECRET is unset or empty it logs a value-free operator warning and throws GrantConfigError, refusing to mint or accept grants, because signing with an empty secret would be silently insecure.","triggerScenarios":"Any call path that mints or verifies an MCP pro grant (e.g. api/internal/mcp-grant-mint.ts mintGrantHandler, api/oauth/authorize-pro.ts) when process.env.MCP_PRO_GRANT_HMAC_SECRET is missing or set to the empty string at runtime.","commonSituations":"Deploying the Edge function without configuring the secret in Vercel env vars; running locally without a .env.local entry; a typo'd env name (e.g. MCP_GRANT_HMAC_SECRET); the secret configured only for one environment (preview but not production); a redeploy that dropped env configuration.","solutions":["Set MCP_PRO_GRANT_HMAC_SECRET in the deployment environment (Vercel project settings or .env.local) to a strong random value, then redeploy/restart.","Verify the exact env name with `vercel env ls` or by printing Object.keys(process.env) filtered on MCP_ in a scratch endpoint — no typos, correct environment scope.","If the value exists but the error persists, confirm the function actually receives env vars (correct project, correct environment: production/preview/development) and that no build-time inlining stripped it.","After rotating or first setting the secret, expect previously issued grants to fail verification; re-mint grants."],"exampleFix":"// before (deploy without secret)\n// vercel deploy  -> readGrantSecret throws GrantConfigError\n// after\necho \"MCP_PRO_GRANT_HMAC_SECRET=$(openssl rand -hex 32)\" >> .env.local\n# or: vercel env add MCP_PRO_GRANT_HMAC_SECRET production\nvercel deploy","handlingStrategy":"validation","validationCode":"function hasGrantSecret(env = process.env): boolean {\n  return typeof env.MCP_PRO_GRANT_HMAC_SECRET === 'string' && env.MCP_PRO_GRANT_HMAC_SECRET.length > 0;\n}\nif (!hasGrantSecret()) throw new Error('Set MCP_PRO_GRANT_HMAC_SECRET before calling grant APIs');","typeGuard":"function isGrantEnvConfigured(env: NodeJS.ProcessEnv): env is NodeJS.ProcessEnv & { MCP_PRO_GRANT_HMAC_SECRET: string } {\n  return typeof env.MCP_PRO_GRANT_HMAC_SECRET === 'string' && env.MCP_PRO_GRANT_HMAC_SECRET.length > 0;\n}","tryCatchPattern":"try {\n  const secret = readGrantSecret();\n  // sign/verify grant\n} catch (err) {\n  if (err instanceof GrantConfigError) {\n    // deployment misconfiguration: fail request with 503, alert operator\n  } else throw err;\n}","preventionTips":["Add MCP_PRO_GRANT_HMAC_SECRET to .env.example and deployment env checklists.","Run a startup readiness check that asserts required env vars exist before serving traffic.","Use the exact env name everywhere; source it from a single constants module.","Rotate the secret via a documented runbook — setting it changes grant validity."],"tags":["config","env-var","hmac","security","mcp"],"backgroundTag":"missing-env-var","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}