// Based on OpenClaw Browser Extension Relay (MIT License) // https://github.com/nicepkg/openclaw function hasCdpVersionShape(data) { return !!data && typeof data === 'object' && 'Browser' in data && 'Protocol-Version' in data } /** * Turn whatever someone typed into a bare host. * * People paste the address they use for Officer, which has a scheme on it and sometimes a port and a * path. Taking it raw produced `http://https://platform.officer.dev:18792/` — a URL that cannot resolve, * reported as "Relay not reachable", which reads as the relay being down rather than the address being * mangled. Reported by the owner, with a screenshot, as "very very bad". * * Returns the host and, when the input carried one, the port — so pasting `host:1234` moves the port * field rather than being silently dropped. * * ── The scheme is always https ── * * Whatever was typed. `http://` in the box becomes https, and nothing becomes https. The relay carries a * pairing token on every request and drives a browser someone is signed into; sending that over plaintext * because a field was left blank is not a default worth having. */ export function normalizeHost(input) { let value = String(input || '').trim() if (!value) return { host: '', port: null } value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') // scheme — discarded, https is forced below value = value.replace(/\/.*$/, '') // path value = value.replace(/^[^@]*@/, '') // credentials, if someone pastes them // IPv6 in brackets keeps its colons; anything else splits on the last one. let port = null const bracketed = value.match(/^\[(.+)\](?::(\d+))?$/) if (bracketed) { value = bracketed[1] if (bracketed[2]) port = Number(bracketed[2]) } else if (/^[^:]+:(\d+)$/.test(value)) { const [h, p] = value.split(':') value = h port = Number(p) } return { host: value, port } } /** The one place a relay URL is spelled. https always — see `normalizeHost`. */ export const relayUrl = (host, port, path = '') => `https://${host}:${port}${path}` export function classifyRelayCheckResponse(res, host, port) { if (!res) { return { action: 'throw', error: 'No response from service worker' } } if (res.status === 401) { return { action: 'status', kind: 'error', message: 'Relay token rejected. Check token and save again.' } } if (res.error) { return { action: 'throw', error: res.error } } if (!res.ok) { return { action: 'throw', error: `HTTP ${res.status}` } } const contentType = String(res.contentType || '') if (!contentType.includes('application/json')) { return { action: 'status', kind: 'error', message: `Wrong endpoint: expected Officer browser relay at this address.`, } } if (!hasCdpVersionShape(res.json)) { const browser = res.json?.Browser ?? '' if (browser && !browser.startsWith('Officer/')) { return { action: 'status', kind: 'error', message: `Wrong relay: found "${browser}" instead of Officer/browser-relay.`, } } return { action: 'status', kind: 'error', message: `Wrong endpoint: expected relay /json/version response.`, } } return { action: 'status', kind: 'ok', message: `Relay reachable and authenticated at ${relayUrl(host, port, '/')}` } } export function classifyRelayCheckException(err, host, port) { const message = String(err || '').toLowerCase() if (message.includes('json') || message.includes('syntax')) { return { kind: 'error', message: `Wrong endpoint: this is not a relay endpoint.`, } } // Say what was actually tried and what usually explains it. "Verify the server address" is true and // useless — the address is usually a real one that simply does not carry this port. return { kind: 'error', message: `Nothing answered at ${relayUrl(host, port, '/')}. The relay listens on the machine Officer runs on, ` + `on a port that is normally reachable only over your private network — a public hostname will not ` + `have it open. Try the address you use to SSH to that machine.`, } }