{"record":{"id":"1194d7f2a06c126d","repo":"thedotmack/claude-mem","slug":"unauthorized-missing-or-invalid-observation-tv-token","errorCode":"Unauthorized","errorMessage":"Missing or invalid Observation TV token","messagePattern":"Missing or invalid Observation TV token","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"src/services/worker/http/middleware.ts","lineNumber":310,"sourceCode":"      // query string in Express, which is what makes that safe.\n      logRemoteDenial({\n        path: req.path,\n        method: req.method,\n        clientIp,\n        reason: decision.reason,\n      });\n      // Always write the response. The worker never calls finalizeRoutes(), so\n      // it has no terminal error handler — forwarding an error to Express\n      // would land in its default handler and return an HTML stack page.\n      if (decision.status === 404) {\n        res.status(404).json({ error: 'Not found' });\n      } else if (decision.status === 403) {\n        res.status(403).json({\n          error: 'Forbidden',\n          message: 'Observation TV remote access is read-only'\n        });\n      } else {\n        res.status(401).json({\n          error: 'Unauthorized',\n          message: 'Missing or invalid Observation TV token'\n        });\n      }\n      return;\n    }\n\n    // 6. Pass.\n    res.setHeader('Cache-Control', 'no-store');\n    next();\n  };\n}\n\nexport function summarizeRequestBody(method: string, path: string, body: any): string {\n  if (!body || Object.keys(body).length === 0) return '';\n\n  if (path.includes('/init')) {\n    return '';","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/worker/http/middleware.ts#L292-L328","documentation":"createRemoteReadOnlyGuard protects the Observation TV remote-access endpoints. When a request lacks a valid TV token and is not an allowed read-only request, the guard rejects it with 401 'Missing or invalid Observation TV token'. The token must be supplied (and match the configured Observation TV token) for remote access.","triggerScenarios":"Calling an Observation TV remote endpoint without an Authorization/token header, with a malformed token, or with a token that does not match the configured value; the guard's decision path falls into the non-403 else branch (status 401).","commonSituations":"TV display client not configured with the pairing token; token rotated or regenerated on the server while the client still caches the old one; proxy stripping the Authorization header; typos when copying the token from settings.","solutions":["Set/copy the current Observation TV token into the client and resend the request with the token header","If the token was rotated, re-pair the TV client with the new token from worker settings","Verify the client sends the header on every request (check that a reverse proxy is not stripping Authorization)","Confirm the endpoint is the remote (read-only) route and you are not hitting it from a context that should use full local access"],"exampleFix":"// before\nfetch('http://worker:37777/api/tv/state');\n// after\nfetch('http://worker:37777/api/tv/state', {\n  headers: { Authorization: `Bearer ${tvToken}` }\n});","handlingStrategy":"validation","validationCode":"const token = process.env.OBSERVATION_TV_TOKEN;\nif (!token) throw new Error('Observation TV token not configured; cannot call remote TV endpoints');","typeGuard":"function hasTvToken(h: Record<string,string>): h is Record<string,string> & { Authorization: string } {\n  return typeof h.Authorization === 'string' && h.Authorization.length > 0;\n}","tryCatchPattern":"try {\n  const res = await fetch(url, { headers: { Authorization: `Bearer ${tvToken}` } });\n  if (res.status === 401) throw new UnauthorizedTvError(await res.text());\n} catch (e) {\n  if (e instanceof UnauthorizedTvError) { /* refresh token / re-pair */ }\n  throw e;\n}","preventionTips":["Load the TV token from config at startup and fail fast if absent","Send the Authorization header via a shared fetch wrapper so no call forgets it","Re-fetch the token after any re-pairing/rotation instead of caching indefinitely","Never proxy TV requests through layers that strip Authorization headers"],"tags":["http","authentication","middleware"],"backgroundTag":"authentication-required","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}