{"record":{"id":"2692252091c994c1","repo":"HeyPuter/puter","slug":"forbidden-269225","errorCode":"forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/controllers/peer/PeerController.ts","lineNumber":231,"sourceCode":"            throw new HttpError(500, 'TURN credential generation failed', {\n                legacyCode: 'internal_error',\n            });\n        }\n\n        const data = (await cfRes.json()) as { iceServers?: unknown };\n        res.json({ ttl, iceServers: data.iceServers });\n    };\n\n    /**\n     * POST /turn/ingest-usage — internal-only TURN egress metering. an external\n     * service that knows the usage information from cloudflare will send it to\n     * us here. Meters each record directly against the owning user via\n     * `services.metering.incrementUsage` multiplied by turn:egress-bytes cost.\n     */\n    #ingestUsage = async (req: Request, res: Response): Promise<void> => {\n        const cfg = this.config.peers;\n        if (!cfg || !cfg.internal_auth_secret) {\n            throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });\n        }\n        const expectedSecret = cfg.internal_auth_secret;\n        const header = req.headers['x-puter-internal-auth'];\n        if (\n            !expectedSecret ||\n            typeof header !== 'string' ||\n            !secretsEqual(header, expectedSecret)\n        ) {\n            throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });\n        }\n\n        const { records } = req.body ?? {};\n        if (!Array.isArray(records)) {\n            throw new HttpError(400, 'Missing `records` array', {\n                legacyCode: 'bad_request',\n            });\n        }\n","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/peer/PeerController.ts#L213-L249","documentation":"`POST /turn/ingest-usage` is an internal-only endpoint authenticated via a shared secret (`config.peers.internal_auth_secret`). If that config value is missing, the endpoint immediately returns 403 — the feature is not set up, so no request can authenticate. This is a configuration guard, not a credential check failure.","triggerScenarios":"The internal usage-ingestion service calls the endpoint, but `config.peers` or `config.peers.internal_auth_secret` is not configured on the server. The first guard (`!cfg || !cfg.internal_auth_secret`) fires before the header is even checked.","commonSituations":"Self-hosting without configuring the internal auth secret; the external metering service is deployed but the server config was never updated; a new deployment that copied a minimal config template.","solutions":["Set `peers.internal_auth_secret` to a strong random string shared between the server and the external usage-ingestion service.","Ensure the external service sends the same value in the `x-puter-internal-auth` header.","Restart the backend after updating config.","If you don't run the usage-ingestion service, this endpoint returning 403 is expected and harmless."],"exampleFix":"// before (config.json)\n{ \"peers\": { \"signaller_url\": \"...\" } }\n\n// after\n{\n  \"peers\": {\n    \"signaller_url\": \"...\",\n    \"internal_auth_secret\": \"<strong-random-secret>\"\n  }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Internal service — catch 403 and alert ops that config is missing\ntry {\n  await fetch('/turn/ingest-usage', {\n    method: 'POST',\n    headers: { 'x-puter-internal-auth': secret },\n    body: JSON.stringify({ records }),\n  });\n} catch (e) {\n  if (e.code === 'forbidden') {\n    console.error('internal_auth_secret not configured on server. Alert ops.');\n  }\n}","preventionTips":["Self-hosters: set `peers.internal_auth_secret` before deploying the ingestion service.","Share the secret securely between the server and ingestion service (not in version control).","If you don't run the ingestion service, 403 here is expected and safe to ignore."],"tags":["webrtc","turn","config","internal-api","forbidden","http-403","security"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}