{"record":{"id":"5c3eaf4637b788bd","repo":"can1357/oh-my-pi","slug":"normalized-error","errorCode":null,"errorMessage":"${normalized.error}","messagePattern":"\\$\\{normalized\\.error\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/collab-web/src/lib/link.ts","lineNumber":130,"sourceCode":"}\n\n/**\n * Render the shareable link. Compact forms: the default relay collapses to\n * `<roomId>.<key>`; custom wss relays drop the scheme (`host[:port]/r/…`);\n * plain-ws localhost relays keep the full `ws://` URL.\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 key.\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\tlet secret = key;\n\tif (writeToken) {\n\t\tsecret = new Uint8Array(key.byteLength + writeToken.byteLength);\n\t\tsecret.set(key, 0);\n\t\tsecret.set(writeToken, key.byteLength);\n\t}\n\tconst keyText = encodeBase64Url(secret);\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\nexport function parseCollabLink(link: string): ParsedCollabLink | { error: string } {\n\t// Lenient input: terminals that open OSC 8 links through strict URL stacks\n\t// (macOS Foundation) percent-encode the legacy second `#` to `%23`.\n\tlet text = link.trim().replace(/%23/gi, \"#\");","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/collab-web/src/lib/link.ts#L112-L148","documentation":"formatCollabLink first normalizes the relay URL via normalizeRelayOrigin; if the URL is not a valid http(s) origin it returns an error object and formatCollabLink throws with that message instead of producing a link. This fails fast rather than emitting a link pointing at a broken relay.","triggerScenarios":"Calling formatCollabLink with a relayUrl lacking a scheme (e.g. 'myrelay.example' instead of 'https://myrelay.example'), containing a path/query, or being an invalid/unparseable URL.","commonSituations":"Storing relay addresses in config without the https:// scheme, users typing 'localhost:8080' into a settings field, environment-specific relay URLs that are empty or contain whitespace.","solutions":["Pass a full origin URL including scheme (https:// or http://)","Pre-normalize user-supplied relay settings: prepend https:// when no scheme is present","Validate with normalizeRelayOrigin before calling and surface a friendly config error","Check server config/env for empty or stale RELAY_URL values"],"exampleFix":"// before\nconst link = formatCollabLink(config.relayUrl, roomId, key); // throws if scheme missing\n// after\nconst relay = config.relayUrl.startsWith(\"http\") ? config.relayUrl : \"https://\" + config.relayUrl;\nconst link = formatCollabLink(relay, roomId, key);","handlingStrategy":"validation","validationCode":"function toRelayOrigin(url: string): string | null {\n  try {\n    const u = new URL(url);\n    return (u.protocol === \"https:\" || u.protocol === \"http:\") && u.pathname === \"/\" ? u.origin : null;\n  } catch {\n    return null;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const link = formatCollabLink(relayUrl, roomId, key);\n} catch (e) {\n  throw new Error(\"Relay URL must be a full http(s) origin, got: \" + relayUrl);\n}","preventionTips":["Store relay config as full origin URLs with scheme","Prepend https:// to scheme-less user input before use","Trim whitespace from config values","Validate relay settings at startup, not at link-generation time"],"tags":["validation","url","collaboration"],"backgroundTag":"invalid-relay-url","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}