fix(collab): switched collab links to dot-joined room secrets
- Changed the link grammar from `<roomId>#<key>` / `host[:port]/r/<roomId>#<key>` to dot-joined `<roomId>.<key>` in `formatCollabLink`/`parseCollabLink` (`packages/coding-agent/src/collab/protocol.ts`) and the collab-web mirror (`packages/collab-web/src/lib/link.ts`): RFC 3986 forbids a raw `#` inside a fragment, so strict URL stacks (macOS Foundation behind terminal click-to-open) percent-encoded the second `#`. - Kept legacy `#`-joined links parseable via `BARE_LINK_RE` and added lenient `%23` → `#` decoding for mangled deep links. - Updated the `ConnectScreen` placeholder, `app.tsx` deep-link comment, `DEFAULT_RELAY_URL` doc in `packages/wire`, `docs/collab.md` examples, and both package CHANGELOGs. - Extended `crypto.test.ts` and `link.test.ts` with dot-joined, legacy-hash, and `%23`-mangled link coverage.
This commit is contained in:
+8
-8
@@ -14,14 +14,14 @@ prints
|
||||
|
||||
```
|
||||
Collab session started!
|
||||
• Join from another terminal: omp join "mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30"
|
||||
• or any web browser: my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30
|
||||
• Join from another terminal: omp join "mgAYTZwEnpRQtca0CTgn-Q.gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30"
|
||||
• or any web browser: my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q.gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30
|
||||
```
|
||||
|
||||
The browser line is click-to-join (an OSC 8 hyperlink to the full `https://` deep link): the relay serves the web guest client at `/`, and the room id + key ride in the URL fragment. From another omp (any directory, any machine), either form works:
|
||||
|
||||
```
|
||||
/join my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJU…
|
||||
/join my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q.gdJU…
|
||||
```
|
||||
|
||||
The guest's previous session is restored on `/leave` (or when the host stops).
|
||||
@@ -42,17 +42,17 @@ The guest's previous session is restored on `/leave` (or when the host stops).
|
||||
|
||||
```
|
||||
https://host[:port]/#<link> → browser deep link (printed by /collab; /join accepts it too)
|
||||
<roomId>#<key> → default relay (my.omp.sh)
|
||||
host[:port]/r/<roomId>#<key> → custom relay, wss:// inferred
|
||||
ws://localhost:7475/r/<roomId>#<key> → plain ws, allowed for localhost only
|
||||
<roomId>.<key> → default relay (my.omp.sh)
|
||||
host[:port]/r/<roomId>.<key> → custom relay, wss:// inferred
|
||||
ws://localhost:7475/r/<roomId>.<key> → plain ws, allowed for localhost only
|
||||
```
|
||||
|
||||
The trailing fragment (`#<key>`) is the room secret, base64url-encoded, in one of two strengths:
|
||||
The trailing `.<key>` part is the room secret, base64url-encoded, in one of two strengths:
|
||||
|
||||
- **Full link** — 48 bytes: the 32-byte AES-256-GCM room key followed by a 16-byte write token. Grants prompting, interrupting, and subagent control.
|
||||
- **View-only link** — the bare 32-byte key, no write token. Grants live read access only. Pre-token links parse as view-only.
|
||||
|
||||
In the browser deep link, everything after the first `#` — room id and key — is a URL fragment: it never appears in any HTTP request, and neither secret is ever sent to the relay.
|
||||
The room secret is dot-joined rather than `#`-joined: RFC 3986 forbids a raw `#` inside a URL fragment, so strict URL stacks (macOS Foundation behind terminal click-to-open) percent-encode a second `#` to `%23` and break the link. Parsers leniently accept the legacy `#` form and the mangled `%23` form. In the browser deep link, everything after the `#` — room id and key — is a URL fragment: it never appears in any HTTP request, and neither secret is ever sent to the relay.
|
||||
|
||||
## End-to-end encryption
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Collab links now dot-join the room secret (`<roomId>.<key>`, `host/r/<roomId>.<key>`) instead of using a second `#`: RFC 3986 forbids a raw `#` inside a URL fragment, so macOS Foundation (behind terminal click-to-open) percent-encoded the browser deep link's second `#` to `%23` and the web client rejected the session. `/join`, `omp join`, and the web client still accept legacy `#`-joined links and leniently decode `%23`-mangled ones
|
||||
|
||||
## [15.12.1] - 2026-06-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -108,11 +108,11 @@ export function rewriteEnvelopePeer(data: Uint8Array, peerId: number): void {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Link format: wss://<host[:port]>/r/<roomId>#<base64url-32-byte-key>
|
||||
// Link format: wss://<host[:port]>/r/<roomId>.<base64url-32-byte-key>
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const ROOM_PATH_RE = /^\/r\/([A-Za-z0-9_-]{10,64})$/;
|
||||
const BARE_LINK_RE = /^([A-Za-z0-9_-]{10,64})#([A-Za-z0-9_-]+)$/;
|
||||
const ROOM_PATH_RE = /^\/r\/([A-Za-z0-9_-]{10,64})(?:\.([A-Za-z0-9_-]+))?$/;
|
||||
const BARE_LINK_RE = /^([A-Za-z0-9_-]{10,64})[#.]([A-Za-z0-9_-]+)$/;
|
||||
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
||||
const LOCAL_HOSTNAMES: Record<string, true> = { localhost: true, "127.0.0.1": true, "::1": true, "[::1]": true };
|
||||
|
||||
@@ -152,11 +152,17 @@ function normalizeRelayOrigin(relayUrl: string): { origin: string } | { error: s
|
||||
|
||||
/**
|
||||
* Render the shareable link. Compact forms: the default relay collapses to
|
||||
* `<roomId>#<key>`, other wss relays drop the scheme (`host[:port]/r/…`);
|
||||
* `<roomId>.<key>`, other wss relays drop the scheme (`host[:port]/r/…`);
|
||||
* only localhost ws:// links keep their full URL so parsing cannot
|
||||
* mis-infer wss.
|
||||
*
|
||||
* Full links append the write token to the key in the fragment
|
||||
* The room secret is dot-joined (`<roomId>.<key>`) rather than `#`-joined:
|
||||
* RFC 3986 forbids a raw `#` inside a fragment, so strict URL stacks (macOS
|
||||
* Foundation behind terminal click-to-open) percent-encode a second `#` to
|
||||
* `%23` and break the link. Parsers still accept the legacy `#` form and the
|
||||
* mangled `%23` form.
|
||||
*
|
||||
* Full links append the write token to the key
|
||||
* (`base64url(key ∥ writeToken)`); read-only (view) links carry the bare
|
||||
* 32-byte key, which is also the pre-token link format.
|
||||
*/
|
||||
@@ -165,11 +171,11 @@ export function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Arr
|
||||
if ("error" in normalized) throw new Error(normalized.error);
|
||||
const secret = writeToken ? Buffer.concat([key, writeToken]) : Buffer.from(key);
|
||||
const keyText = secret.toString("base64url");
|
||||
if (normalized.origin === DEFAULT_RELAY_URL) return `${roomId}#${keyText}`;
|
||||
if (normalized.origin === DEFAULT_RELAY_URL) return `${roomId}.${keyText}`;
|
||||
const compact = normalized.origin.startsWith("wss://")
|
||||
? normalized.origin.slice("wss://".length)
|
||||
: normalized.origin;
|
||||
return `${compact}/r/${roomId}#${keyText}`;
|
||||
return `${compact}/r/${roomId}.${keyText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,10 +199,12 @@ export function formatCollabWebLink(
|
||||
}
|
||||
|
||||
export function parseCollabLink(link: string): ParsedCollabLink | { error: string } {
|
||||
let text = link.trim();
|
||||
// Bare `<roomId>#<key>` → default relay.
|
||||
// Lenient input: terminals that open OSC 8 links through strict URL stacks
|
||||
// (macOS Foundation) percent-encode the legacy second `#` to `%23`.
|
||||
let text = link.trim().replace(/%23/gi, "#");
|
||||
// Bare `<roomId>.<key>` (legacy `<roomId>#<key>`) → default relay.
|
||||
const bare = BARE_LINK_RE.exec(text);
|
||||
if (bare) text = `${DEFAULT_RELAY_URL}/r/${bare[1]}#${bare[2]}`;
|
||||
if (bare) text = `${DEFAULT_RELAY_URL}/r/${bare[1]}.${bare[2]}`;
|
||||
// Scheme-less `host[:port]/r/…` → wss.
|
||||
else if (!text.includes("://")) text = `wss://${text}`;
|
||||
let url: URL;
|
||||
@@ -210,17 +218,18 @@ export function parseCollabLink(link: string): ParsedCollabLink | { error: strin
|
||||
const match = ROOM_PATH_RE.exec(url.pathname);
|
||||
if (!match) {
|
||||
// Web deep link: `http(s)://<relay>/#<collab-link>` — the fragment holds
|
||||
// the whole link (which itself contains another `#`). URL.hash spans to
|
||||
// the end of the string, so recurse on it; the inner fragment is just
|
||||
// the key (no `#`), which bounds the recursion.
|
||||
// the whole link, so recurse on it. The recursion terminates because
|
||||
// the inner text is a strict suffix of the input.
|
||||
const inner = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
||||
if (inner.includes("#")) return parseCollabLink(inner);
|
||||
if (inner) return parseCollabLink(inner);
|
||||
return { error: "Collab link must contain a /r/<roomId> path" };
|
||||
}
|
||||
const roomId = match[1]!;
|
||||
const fragment = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
||||
// Key rides dot-joined in the path (`/r/<roomId>.<key>`); legacy links
|
||||
// carry it in the fragment (`/r/<roomId>#<key>`).
|
||||
const fragment = match[2] ?? (url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
|
||||
if (!fragment) {
|
||||
return { error: "Collab link is missing the #<key> fragment" };
|
||||
return { error: "Collab link is missing the <key> part" };
|
||||
}
|
||||
const secret = B64URL_RE.test(fragment) ? new Uint8Array(Buffer.from(fragment, "base64url")) : null;
|
||||
if (!secret || (secret.byteLength !== ROOM_KEY_BYTES && secret.byteLength !== ROOM_KEY_BYTES + WRITE_TOKEN_BYTES)) {
|
||||
|
||||
@@ -44,9 +44,9 @@ describe("collab link format", () => {
|
||||
const key = generateRoomKey();
|
||||
const roomId = generateRoomId();
|
||||
|
||||
it("collapses the default relay to a bare roomId#key link", () => {
|
||||
it("collapses the default relay to a bare roomId.key link", () => {
|
||||
const link = formatCollabLink(DEFAULT_RELAY_URL, roomId, key);
|
||||
expect(link).toBe(`${roomId}#${Buffer.from(key).toString("base64url")}`);
|
||||
expect(link).toBe(`${roomId}.${Buffer.from(key).toString("base64url")}`);
|
||||
const parsed = parseCollabLink(link);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${roomId}`);
|
||||
@@ -82,8 +82,30 @@ describe("collab link format", () => {
|
||||
});
|
||||
|
||||
it("rejects keys that are not 32 base64url bytes", () => {
|
||||
expect("error" in parseCollabLink(`${roomId}#dG9vc2hvcnQ`)).toBe(true);
|
||||
expect("error" in parseCollabLink(`${roomId}#not+base64url/`)).toBe(true);
|
||||
expect("error" in parseCollabLink(`${roomId}.dG9vc2hvcnQ`)).toBe(true);
|
||||
expect("error" in parseCollabLink(`${roomId}.not+base64url/`)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts legacy #-separated links", () => {
|
||||
const keyText = Buffer.from(key).toString("base64url");
|
||||
for (const legacy of [
|
||||
`${roomId}#${keyText}`,
|
||||
`relay.example.com:8443/r/${roomId}#${keyText}`,
|
||||
`https://my.omp.sh/#${roomId}#${keyText}`,
|
||||
]) {
|
||||
const parsed = parseCollabLink(legacy);
|
||||
if ("error" in parsed) throw new Error(`${legacy}: ${parsed.error}`);
|
||||
expect(parsed.roomId).toBe(roomId);
|
||||
expect(Buffer.from(parsed.key)).toEqual(Buffer.from(key));
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts %23-mangled legacy deep links (macOS Foundation re-encoding)", () => {
|
||||
const keyText = Buffer.from(key).toString("base64url");
|
||||
const parsed = parseCollabLink(`https://my.omp.sh/#${roomId}%23${keyText}`);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${roomId}`);
|
||||
expect(Buffer.from(parsed.key)).toEqual(Buffer.from(key));
|
||||
});
|
||||
|
||||
it("renders web deep links that parse back to the same room", () => {
|
||||
@@ -140,7 +162,7 @@ describe("collab link format", () => {
|
||||
expect(url.origin).toBe("https://my.omp.sh");
|
||||
expect(url.pathname).toBe("/");
|
||||
expect(url.search).toBe("");
|
||||
expect(url.hash).toBe(`#${roomId}#${Buffer.from(key).toString("base64url")}`);
|
||||
expect(url.hash).toBe(`#${roomId}.${Buffer.from(key).toString("base64url")}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Link parsing accepts the new dot-joined room secret (`<roomId>.<key>`, `/r/<roomId>.<key>`) and leniently decodes `%23`-mangled legacy deep links (macOS Foundation percent-encodes a second `#` when terminals open clicked links), which previously failed to connect
|
||||
|
||||
## [15.12.0] - 2026-06-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -12,7 +12,7 @@ bun run dev
|
||||
bun run mock-host
|
||||
```
|
||||
|
||||
Host a session from any omp instance (`/collab`, or `/collab ws://localhost:7466` to use the mock relay), then paste the printed link into the connect screen. Deep links work too: `http://localhost:3000/#<roomId>#<key>` auto-connects on load.
|
||||
Host a session from any omp instance (`/collab`, or `/collab ws://localhost:7466` to use the mock relay), then paste the printed link into the connect screen. Deep links work too: `http://localhost:3000/#<roomId>.<key>` auto-connects on load.
|
||||
|
||||
## Build & deploy
|
||||
|
||||
@@ -23,7 +23,7 @@ bun run build # static site in dist/
|
||||
`dist/` is a fully static SPA — host it anywhere. JS/CSS bundles are content-hashed; favicons, `manifest.webmanifest`, `robots.txt`, `sitemap.xml`, and `og-image.png` come from `public/` and are emitted at the site root under stable names (canonical URL: `https://my.omp.sh/`). Two runtime requirements:
|
||||
|
||||
- **Secure context**: room keys are unwrapped with WebCrypto (`crypto.subtle`), which browsers expose only on `https://` or `localhost`.
|
||||
- **Relay reachability**: the client connects straight to the relay over WebSocket (`wss://` for anything that isn't localhost). The default relay is `wss://my.omp.sh`; bare `<roomId>#<key>` links resolve against it.
|
||||
- **Relay reachability**: the client connects straight to the relay over WebSocket (`wss://` for anything that isn't localhost). The default relay is `wss://my.omp.sh`; bare `<roomId>.<key>` links resolve against it (legacy `<roomId>#<key>` and `%23`-mangled links still parse).
|
||||
|
||||
The room key never leaves the URL fragment — it is not sent to the relay or any server.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function storedName(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** Deep link = everything after the FIRST `#` (the link itself contains another `#`). */
|
||||
/** Deep link = everything after the FIRST `#` (legacy links carry a second `#` inside the fragment). */
|
||||
function hashLink(): string | null {
|
||||
const href = window.location.href;
|
||||
const i = href.indexOf("#");
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ConnectScreen({ defaultName, error, onConnect }: ConnectScreenPr
|
||||
type="text"
|
||||
value={link}
|
||||
onChange={e => setLink(e.target.value)}
|
||||
placeholder="ws://host:port/r/room#key"
|
||||
placeholder="ws://host:port/r/room.key"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* link/envelope half of `@oh-my-pi/pi-coding-agent/src/collab/protocol.ts`;
|
||||
* base64url goes through atob/btoa instead of Buffer).
|
||||
*
|
||||
* Link format: `wss://<host[:port]>/r/<roomId>#<base64url-32-byte-key>`
|
||||
* Link format: `wss://<host[:port]>/r/<roomId>.<base64url-32-byte-key>`
|
||||
* Wire envelope: `[4B uint32 BE peerId][sealed payload]` — the guest always
|
||||
* sends peerId 0; the relay rewrites it to the sender's id.
|
||||
*/
|
||||
@@ -21,8 +21,8 @@ export { COLLAB_PROTO } from "@oh-my-pi/pi-wire";
|
||||
export type { ParsedCollabLink };
|
||||
export { DEFAULT_RELAY_URL, ENVELOPE_HEADER_LENGTH, ROOM_ID_BYTES };
|
||||
|
||||
const ROOM_PATH_RE = /^\/r\/([A-Za-z0-9_-]{10,64})$/;
|
||||
const BARE_LINK_RE = /^([A-Za-z0-9_-]{10,64})#([A-Za-z0-9_-]+)$/;
|
||||
const ROOM_PATH_RE = /^\/r\/([A-Za-z0-9_-]{10,64})(?:\.([A-Za-z0-9_-]+))?$/;
|
||||
const BARE_LINK_RE = /^([A-Za-z0-9_-]{10,64})[#.]([A-Za-z0-9_-]+)$/;
|
||||
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
||||
const LOCAL_HOSTNAMES: Record<string, true> = { localhost: true, "127.0.0.1": true, "::1": true, "[::1]": true };
|
||||
|
||||
@@ -113,10 +113,16 @@ function normalizeRelayOrigin(relayUrl: string): { origin: string } | { error: s
|
||||
|
||||
/**
|
||||
* Render the shareable link. Compact forms: the default relay collapses to
|
||||
* `<roomId>#<key>`; custom wss relays drop the scheme (`host[:port]/r/…`);
|
||||
* `<roomId>.<key>`; custom wss relays drop the scheme (`host[:port]/r/…`);
|
||||
* plain-ws localhost relays keep the full `ws://` URL.
|
||||
*
|
||||
* Full links append the write token to the key in the fragment
|
||||
* The room secret is dot-joined (`<roomId>.<key>`) rather than `#`-joined:
|
||||
* RFC 3986 forbids a raw `#` inside a fragment, so strict URL stacks (macOS
|
||||
* Foundation behind terminal click-to-open) percent-encode a second `#` to
|
||||
* `%23` and break the link. Parsers still accept the legacy `#` form and the
|
||||
* mangled `%23` form.
|
||||
*
|
||||
* Full links append the write token to the key
|
||||
* (`base64url(key ∥ writeToken)`); read-only (view) links carry the bare key.
|
||||
*/
|
||||
export function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Array, writeToken?: Uint8Array): string {
|
||||
@@ -129,18 +135,20 @@ export function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Arr
|
||||
secret.set(writeToken, key.byteLength);
|
||||
}
|
||||
const keyText = encodeBase64Url(secret);
|
||||
if (normalized.origin === DEFAULT_RELAY_URL) return `${roomId}#${keyText}`;
|
||||
if (normalized.origin === DEFAULT_RELAY_URL) return `${roomId}.${keyText}`;
|
||||
const compact = normalized.origin.startsWith("wss://")
|
||||
? normalized.origin.slice("wss://".length)
|
||||
: normalized.origin;
|
||||
return `${compact}/r/${roomId}#${keyText}`;
|
||||
return `${compact}/r/${roomId}.${keyText}`;
|
||||
}
|
||||
|
||||
export function parseCollabLink(link: string): ParsedCollabLink | { error: string } {
|
||||
let text = link.trim();
|
||||
// Bare `<roomId>#<key>` → default relay.
|
||||
// Lenient input: terminals that open OSC 8 links through strict URL stacks
|
||||
// (macOS Foundation) percent-encode the legacy second `#` to `%23`.
|
||||
let text = link.trim().replace(/%23/gi, "#");
|
||||
// Bare `<roomId>.<key>` (legacy `<roomId>#<key>`) → default relay.
|
||||
const bare = BARE_LINK_RE.exec(text);
|
||||
if (bare) text = `${DEFAULT_RELAY_URL}/r/${bare[1]}#${bare[2]}`;
|
||||
if (bare) text = `${DEFAULT_RELAY_URL}/r/${bare[1]}.${bare[2]}`;
|
||||
// Scheme-less `host[:port]/r/…` → wss.
|
||||
else if (!text.includes("://")) text = `wss://${text}`;
|
||||
let url: URL;
|
||||
@@ -154,17 +162,18 @@ export function parseCollabLink(link: string): ParsedCollabLink | { error: strin
|
||||
const match = ROOM_PATH_RE.exec(url.pathname);
|
||||
if (!match) {
|
||||
// Web deep link: `http(s)://<relay>/#<collab-link>` — the fragment holds
|
||||
// the whole link (which itself contains another `#`). URL.hash spans to
|
||||
// the end of the string, so recurse on it; the inner fragment is just
|
||||
// the key (no `#`), which bounds the recursion.
|
||||
// the whole link, so recurse on it. The recursion terminates because
|
||||
// the inner text is a strict suffix of the input.
|
||||
const inner = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
||||
if (inner.includes("#")) return parseCollabLink(inner);
|
||||
if (inner) return parseCollabLink(inner);
|
||||
return { error: "Collab link must contain a /r/<roomId> path" };
|
||||
}
|
||||
const roomId = match[1] as string;
|
||||
const fragment = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
||||
// Key rides dot-joined in the path (`/r/<roomId>.<key>`); legacy links
|
||||
// carry it in the fragment (`/r/<roomId>#<key>`).
|
||||
const fragment = match[2] ?? (url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
|
||||
if (!fragment) {
|
||||
return { error: "Collab link is missing the #<key> fragment" };
|
||||
return { error: "Collab link is missing the <key> part" };
|
||||
}
|
||||
const secret = decodeBase64Url(fragment);
|
||||
if (!secret || (secret.byteLength !== ROOM_KEY_BYTES && secret.byteLength !== ROOM_KEY_BYTES + WRITE_TOKEN_BYTES)) {
|
||||
|
||||
@@ -15,7 +15,15 @@ const KEY_TEXT = encodeBase64Url(KEY);
|
||||
const ROOM = "AbCdEf123456_-Xy";
|
||||
|
||||
describe("collab link parsing", () => {
|
||||
it("parses a bare roomId#key link against the default relay", () => {
|
||||
it("parses a bare roomId.key link against the default relay", () => {
|
||||
const parsed = parseCollabLink(`${ROOM}.${KEY_TEXT}`);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${ROOM}`);
|
||||
expect(parsed.roomId).toBe(ROOM);
|
||||
expect(parsed.key).toEqual(KEY);
|
||||
});
|
||||
|
||||
it("parses a legacy bare roomId#key link against the default relay", () => {
|
||||
const parsed = parseCollabLink(`${ROOM}#${KEY_TEXT}`);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${ROOM}`);
|
||||
@@ -74,6 +82,20 @@ describe("collab link parsing", () => {
|
||||
expect(local.wsUrl).toBe(`ws://localhost:7466/r/${ROOM}`);
|
||||
});
|
||||
|
||||
it("parses dot-joined web deep links (https://<relay>/#<roomId>.<key>)", () => {
|
||||
const parsed = parseCollabLink(`https://my.omp.sh/#${ROOM}.${KEY_TEXT}`);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${ROOM}`);
|
||||
expect(parsed.key).toEqual(KEY);
|
||||
});
|
||||
|
||||
it("accepts %23-mangled legacy deep links (macOS Foundation re-encoding)", () => {
|
||||
const parsed = parseCollabLink(`https://my.omp.sh/#${ROOM}%23${KEY_TEXT}`);
|
||||
if ("error" in parsed) throw new Error(parsed.error);
|
||||
expect(parsed.wsUrl).toBe(`${DEFAULT_RELAY_URL}/r/${ROOM}`);
|
||||
expect(parsed.key).toEqual(KEY);
|
||||
});
|
||||
|
||||
it("round-trips format → parse for default, custom, and localhost relays", () => {
|
||||
const roomId = generateRoomId();
|
||||
for (const relay of [DEFAULT_RELAY_URL, "wss://relay.example.com:8443", "ws://127.0.0.1:7466"]) {
|
||||
|
||||
@@ -363,7 +363,7 @@ export const ROOM_KEY_BYTES = 32;
|
||||
*/
|
||||
export const WRITE_TOKEN_BYTES = 16;
|
||||
|
||||
/** Default public relay; bare `<roomId>#<key>` links resolve against it. */
|
||||
/** Default public relay; bare `<roomId>.<key>` links resolve against it. */
|
||||
export const DEFAULT_RELAY_URL = "wss://my.omp.sh";
|
||||
|
||||
/** Default share viewer/upload base; `/share` links resolve against `<base>/<id>#<key>`. */
|
||||
|
||||
Reference in New Issue
Block a user