{"record":{"id":"2447142e2bd852df","repo":"can1357/oh-my-pi","slug":"imageurls-exposure-named-cloudflared-requires-cr","errorCode":null,"errorMessage":"imageUrls exposure \"named-cloudflared\" requires credentials.tunnelToken or options.configFile and options.tunnelName","messagePattern":"imageUrls exposure \"named-cloudflared\" requires credentials\\.tunnelToken or options\\.configFile and options\\.tunnelName","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/exposure.ts","lineNumber":473,"sourceCode":"\t\t\tconst argv = [binary, \"local\", String(port), \"--to\", server];\n\t\t\tif (secret) argv.push(\"--secret\", secret);\n\t\t\tconst { proc, baseUrl } = await spawnUrlTunnel(argv, line => parseBoreUrl(line, server));\n\t\t\treturn processExposure(\"bore\", baseUrl, proc);\n\t\t}\n\t\tcase \"named-cloudflared\": {\n\t\t\tif (!config.publicBaseUrl) {\n\t\t\t\tthrow new Error('imageUrls exposure \"named-cloudflared\" requires imageUrls.publicBaseUrl');\n\t\t\t}\n\t\t\tconst binary = requireBinary(\"cloudflared\");\n\t\t\tconst token = credentialString(config, \"tunnelToken\");\n\t\t\tlet argv: string[];\n\t\t\tif (token) {\n\t\t\t\targv = [binary, \"tunnel\", \"--no-autoupdate\", \"run\", \"--token\", token];\n\t\t\t} else {\n\t\t\t\tconst configFile = optionString(config, \"configFile\");\n\t\t\t\tconst tunnelName = optionString(config, \"tunnelName\");\n\t\t\t\tif (!configFile || !tunnelName) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'imageUrls exposure \"named-cloudflared\" requires credentials.tunnelToken or options.configFile and options.tunnelName',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\targv = [binary, \"tunnel\", \"--no-autoupdate\", \"--config\", configFile, \"run\", tunnelName];\n\t\t\t}\n\t\t\tconst baseUrl = normalizeBaseUrl(config.publicBaseUrl);\n\t\t\tconst { proc } = await spawnUrlTunnel(\n\t\t\t\targv,\n\t\t\t\t() => baseUrl,\n\t\t\t\t/Registered tunnel connection|Connection [a-z0-9-]+ registered/i,\n\t\t\t);\n\t\t\treturn processExposure(\"named-cloudflared\", baseUrl, proc);\n\t\t}\n\t\tcase \"ssh\": {\n\t\t\tif (!config.publicBaseUrl) throw new Error('imageUrls exposure \"ssh\" requires imageUrls.publicBaseUrl');\n\t\t\tif (!config.sshTarget) throw new Error('imageUrls exposure \"ssh\" requires imageUrls.sshTarget');\n\t\t\tconst binary = requireBinary(\"ssh\");\n\t\t\tconst remotePort = config.sshRemotePort ?? 8787;","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/exposure.ts#L455-L491","documentation":"A named Cloudflare Tunnel must be authenticated two ways: with a tunnel token, or with a config file plus the tunnel name. This error is thrown when neither credentials.tunnelToken nor the pair options.configFile + options.tunnelName is provided. It validates before spawning cloudflared so it fails fast instead of cloudflared exiting with its own cryptic error.","triggerScenarios":"kind \"named-cloudflared\" with publicBaseUrl set, but no credentials.tunnelToken and either options.configFile or options.tunnelName missing/empty.","commonSituations":"User created a tunnel in the Cloudflare dashboard but only copied the publicBaseUrl; config file path typo'd so optionString returns undefined; user supplied configFile but forgot tunnelName; token from `cloudflared tunnel token <name>` never pasted into credentials.","solutions":["Set credentials.tunnelToken from `cloudflared tunnel token <tunnel-name>` — the simplest path.","Or provide both options.configFile (path to cloudflared config.yml) and options.tunnelName.","Verify the values are non-empty strings and under the right keys (credentials vs options).","Run `cloudflared tunnel list` to confirm the tunnel exists."],"exampleFix":"// before\n\"named-cloudflared\": { \"options\": { \"configFile\": \"/etc/cloudflared/config.yml\" } }\n// after\n\"named-cloudflared\": { \"options\": { \"configFile\": \"/etc/cloudflared/config.yml\", \"tunnelName\": \"omp-files\" } }","handlingStrategy":"validation","validationCode":"if (config.kind === \"named-cloudflared\") {\n  const hasToken = typeof config.credentials?.tunnelToken === \"string\" && config.credentials.tunnelToken.length > 0;\n  const hasPair = typeof config.options?.configFile === \"string\" && config.options.configFile.length > 0 &&\n    typeof config.options?.tunnelName === \"string\" && config.options.tunnelName.length > 0;\n  if (!hasToken && !hasPair) throw new Error(\"named-cloudflared: set credentials.tunnelToken or options.configFile+tunnelName\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Prefer credentials.tunnelToken — single value, no file path coupling","Store the token via your secrets mechanism, never in plaintext logs","If using configFile, verify the file exists and the tunnelName matches `cloudflared tunnel list`","Validate the exposure config before starting the broker"],"tags":["configuration","cloudflared","credentials","validation"],"backgroundTag":"missing-credentials","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}