{"record":{"id":"6f6523ab7468816f","repo":"can1357/oh-my-pi","slug":"collab-weburl-must-use-https-unless-it-targets","errorCode":null,"errorMessage":"collab.webUrl must use https:// unless it targets localhost","messagePattern":"collab\\.webUrl must use https:// unless it targets localhost","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/collab/protocol.ts","lineNumber":227,"sourceCode":"\tif (!explicitWebUrl) {\n\t\tconst normalized = normalizeRelayOrigin(relayUrl);\n\t\tif (\"error\" in normalized) throw new Error(normalized.error);\n\t\treturn normalized.origin.startsWith(\"wss://\")\n\t\t\t? `https://${normalized.origin.slice(\"wss://\".length)}`\n\t\t\t: `http://${normalized.origin.slice(\"ws://\".length)}`;\n\t}\n\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(explicitWebUrl);\n\t} catch {\n\t\tthrow new Error(\"collab.webUrl must start with http:// or https://\");\n\t}\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new Error(\"collab.webUrl must start with http:// or https://\");\n\t}\n\tif (url.protocol === \"http:\" && !isLocalHostname(url.hostname)) {\n\t\tthrow new Error(\"collab.webUrl must use https:// unless it targets localhost\");\n\t}\n\tif (url.search || url.hash) {\n\t\tthrow new Error(\"collab.webUrl must not include a query string or fragment\");\n\t}\n\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\treturn `${url.origin}${path}`;\n}\n\n/**\n * Render the browser deep link. The browser UI may be hosted separately from\n * the relay; the fragment always carries the relay-specific collab link, so\n * room secrets stay out of HTTP path and query bytes.\n */\nexport function formatCollabWebLink(\n\trelayUrl: string,\n\troomId: string,\n\tkey: Uint8Array,\n\twriteToken?: Uint8Array,","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/collab/protocol.ts#L209-L245","documentation":"The web link base must be encrypted in production: normalizeCollabWebBaseUrl allows plain http:// only when the hostname is localhost, 127.0.0.1, or ::1. An http:// URL aimed at any other host is rejected so room secrets (carried in the link fragment) are never sent over an unencrypted connection to a remote web UI.","triggerScenarios":"Calling formatCollabWebLink with webUrl like 'http://collab.example.com' or 'http://192.168.1.10:3000' — valid http(s) URLs, but plain http to a non-local hostname.","commonSituations":"Self-hosting the web UI behind a LAN IP or internal DNS name without TLS; local testing against a container name like http://web:3000 (not recognized as local); forgetting to front the UI with TLS before sharing links.","solutions":["Serve the web UI over https:// (any TLS-terminating proxy or the host's own certificate) and update webUrl accordingly.","For pure local testing, use http://localhost:PORT (or 127.0.0.1/::1), which is the allowed exception.","If testing from other machines on the LAN, set up a self-signed or real certificate and use https even internally.","Use the hostname literally 'localhost' in the URL, not a container/hostname alias, when you intend the local exception."],"exampleFix":"// before\nwebUrl: \"http://collab.example.com\"\n// after\nwebUrl: \"https://collab.example.com\"","handlingStrategy":"validation","validationCode":"const u = new URL(webUrl);\nconst local = [\"localhost\", \"127.0.0.1\", \"::1\"].includes(u.hostname);\nif (u.protocol === \"http:\" && !local) throw new Error(\"webUrl must use https:// (http only allowed for localhost)\");\nformatCollabWebLink(relayUrl, roomId, key, token, webUrl);","typeGuard":null,"tryCatchPattern":"try {\n  const webLink = formatCollabWebLink(relayUrl, roomId, key, token, webUrl);\n} catch (err) {\n  ui.showError(`Insecure webUrl: ${(err as Error).message}`);\n}","preventionTips":["Put the web UI behind TLS before sharing links outside localhost.","Use http://localhost:PORT for local development only.","Treat http:// + non-local host as a config error in your own validation."],"tags":["security","tls","configuration","collaboration"],"backgroundTag":"insecure-http-not-allowed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}