{"record":{"id":"e554a89bdb09085c","repo":"can1357/oh-my-pi","slug":"error-e554a8","errorCode":null,"errorMessage":"error","messagePattern":"error","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/collab/protocol.ts","lineNumber":197,"sourceCode":"/**\n * Render the shareable link. Compact forms: the default relay collapses to\n * `<roomId>.<key>`, other wss relays drop the scheme (`host[:port]/r/…`);\n * only localhost ws:// links keep their full URL so parsing cannot\n * mis-infer wss.\n *\n * The room secret is dot-joined (`<roomId>.<key>`) rather than `#`-joined:\n * RFC 3986 forbids a raw `#` inside a fragment, so strict URL stacks (macOS\n * Foundation behind terminal click-to-open) percent-encode a second `#` to\n * `%23` and break the link. Parsers still accept the legacy `#` form and the\n * mangled `%23` form.\n *\n * Full links append the write token to the key\n * (`base64url(key ∥ writeToken)`); read-only (view) links carry the bare\n * 32-byte key, which is also the pre-token link format.\n */\nexport function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Array, writeToken?: Uint8Array): string {\n\tconst normalized = normalizeRelayOrigin(relayUrl);\n\tif (\"error\" in normalized) throw new Error(normalized.error);\n\tconst secret = writeToken ? Buffer.concat([key, writeToken]) : Buffer.from(key);\n\tconst keyText = secret.toString(\"base64url\");\n\tif (normalized.origin === DEFAULT_RELAY_URL) return `${roomId}.${keyText}`;\n\tconst compact = normalized.origin.startsWith(\"wss://\")\n\t\t? normalized.origin.slice(\"wss://\".length)\n\t\t: normalized.origin;\n\treturn `${compact}/r/${roomId}.${keyText}`;\n}\n\nfunction normalizeCollabWebBaseUrl(relayUrl: string, webUrl?: string): string {\n\tconst explicitWebUrl = webUrl?.trim();\n\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}","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/collab/protocol.ts#L179-L215","documentation":"formatCollabLink normalizes the relay URL before rendering the shareable link and throws when normalizeRelayOrigin returns an error — an unparseable URL, an unsupported scheme, or ws:// pointing at a non-localhost host. This is the same relay-URL validation as the host-start path, surfaced at link formatting time.","triggerScenarios":"Calling formatCollabLink(relayUrl, roomId, key[, writeToken]) with relayUrl that is not a valid absolute URL, has a scheme other than ws/wss/http/https, or uses ws:// with a non-local hostname. Also reachable via formatCollabWebLink, which calls it after building the web base URL.","commonSituations":"Environment/config variable for the relay is empty or a bare hostname; a self-hosted relay configured as 'http://relay:8080' works, but 'ftp://relay' or a typo ('wss//relay') fails; ws:// used for a remote relay in a dev environment with a real hostname.","solutions":["Pass a complete ws:// or wss:// URL with hostname (and optional port) as relayUrl.","For remote relays use wss://; reserve ws:// for localhost/127.0.0.1/::1 only.","Validate with new URL(relayUrl) and check url.protocol in [\"ws:\",\"wss:\",\"http:\",\"https:\"] before calling.","Use the DEFAULT_RELAY_URL (empty relayUrl handling in the caller) if self-hosting is not intended."],"exampleFix":"// before\nformatCollabLink(\"ws://relay.example.com\", roomId, key); // plain ws off localhost\n// after\nformatCollabLink(\"wss://relay.example.com\", roomId, key);","handlingStrategy":"validation","validationCode":"const u = new URL(relayUrl); // throws if invalid\nconst schemeOk = [\"ws:\", \"wss:\", \"http:\", \"https:\"].includes(u.protocol);\nconst localOk = u.protocol !== \"ws:\" || [\"localhost\", \"127.0.0.1\", \"::1\"].includes(u.hostname);\nif (!schemeOk || !localOk) throw new Error(`unsupported relayUrl: ${relayUrl}`);\nformatCollabLink(relayUrl, roomId, key, token);","typeGuard":null,"tryCatchPattern":"try {\n  const link = formatCollabLink(relayUrl, roomId, key, token);\n} catch (err) {\n  ui.showError(`Cannot format link: ${(err as Error).message}`);\n}","preventionTips":["Normalize relay URLs (scheme + host + optional port) at the edge of your app once, then reuse.","Prefer the default relay (omit/empty relayUrl) unless self-hosting deliberately.","Add a config-load-time check that relayUrl parses with an allowed scheme."],"tags":["configuration","url-validation","collaboration"],"backgroundTag":"invalid-url-config","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}