mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:08:37 +00:00
fix(browser): land PR #22571 with safe extension handshake handling
Bind relay WS message handling before onopen and add non-blocking connect.challenge response support without forcing handshake waits on current relay protocol. Landed from contributor @pandego (PR #22571). Co-authored-by: pandego <7780875+pandego@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Browser/Extension relay CORS: handle `/json*` `OPTIONS` preflight before auth checks, allow Chrome extension origins, and return extension-origin CORS headers on relay HTTP responses so extension token validation no longer fails cross-origin. Landed from contributor PR #23962 by @miloudbelarebia. (#23842)
|
- Browser/Extension relay CORS: handle `/json*` `OPTIONS` preflight before auth checks, allow Chrome extension origins, and return extension-origin CORS headers on relay HTTP responses so extension token validation no longer fails cross-origin. Landed from contributor PR #23962 by @miloudbelarebia. (#23842)
|
||||||
- Browser/Extension relay auth: allow `?token=` query-param auth on relay `/json*` endpoints (consistent with relay WebSocket auth) so curl/devtools-style `/json/version` and `/json/list` probes work without requiring custom headers. Landed from contributor PR #26015 by @Sid-Qin. (#25928)
|
- Browser/Extension relay auth: allow `?token=` query-param auth on relay `/json*` endpoints (consistent with relay WebSocket auth) so curl/devtools-style `/json/version` and `/json/list` probes work without requiring custom headers. Landed from contributor PR #26015 by @Sid-Qin. (#25928)
|
||||||
- Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay `stop()` before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng.
|
- Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay `stop()` before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng.
|
||||||
|
- Browser/Chrome extension handshake: bind relay WS message handling before `onopen` and add non-blocking `connect.challenge` response handling for gateway-style handshake frames, avoiding stuck `…` badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)
|
||||||
- Auth/Auth profiles: normalize `auth-profiles.json` alias fields (`mode -> type`, `apiKey -> key`) before credential validation so entries copied from `openclaw.json` auth examples are no longer silently dropped. (#26950) thanks @byungsker.
|
- Auth/Auth profiles: normalize `auth-profiles.json` alias fields (`mode -> type`, `apiKey -> key`) before credential validation so entries copied from `openclaw.json` auth examples are no longer silently dropped. (#26950) thanks @byungsker.
|
||||||
- Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)
|
- Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)
|
||||||
- iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.
|
- iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ const BADGE = {
|
|||||||
let relayWs = null
|
let relayWs = null
|
||||||
/** @type {Promise<void>|null} */
|
/** @type {Promise<void>|null} */
|
||||||
let relayConnectPromise = null
|
let relayConnectPromise = null
|
||||||
|
let relayGatewayToken = ''
|
||||||
|
/** @type {string|null} */
|
||||||
|
let relayConnectRequestId = null
|
||||||
|
|
||||||
let nextSession = 1
|
let nextSession = 1
|
||||||
|
|
||||||
@@ -143,6 +146,13 @@ async function ensureRelayConnection() {
|
|||||||
|
|
||||||
const ws = new WebSocket(wsUrl)
|
const ws = new WebSocket(wsUrl)
|
||||||
relayWs = ws
|
relayWs = ws
|
||||||
|
relayGatewayToken = gatewayToken
|
||||||
|
// Bind message handler before open so an immediate first frame (for example
|
||||||
|
// gateway connect.challenge) cannot be missed.
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
if (ws !== relayWs) return
|
||||||
|
void whenReady(() => onRelayMessage(String(event.data || '')))
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const t = setTimeout(() => reject(new Error('WebSocket connect timeout')), 5000)
|
const t = setTimeout(() => reject(new Error('WebSocket connect timeout')), 5000)
|
||||||
@@ -162,10 +172,6 @@ async function ensureRelayConnection() {
|
|||||||
|
|
||||||
// Bind permanent handlers. Guard against stale socket: if this WS was
|
// Bind permanent handlers. Guard against stale socket: if this WS was
|
||||||
// replaced before its close fires, the handler is a no-op.
|
// replaced before its close fires, the handler is a no-op.
|
||||||
ws.onmessage = (event) => {
|
|
||||||
if (ws !== relayWs) return
|
|
||||||
void whenReady(() => onRelayMessage(String(event.data || '')))
|
|
||||||
}
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
if (ws !== relayWs) return
|
if (ws !== relayWs) return
|
||||||
onRelayClosed('closed')
|
onRelayClosed('closed')
|
||||||
@@ -188,6 +194,8 @@ async function ensureRelayConnection() {
|
|||||||
// Debugger sessions are kept alive so they survive transient WS drops.
|
// Debugger sessions are kept alive so they survive transient WS drops.
|
||||||
function onRelayClosed(reason) {
|
function onRelayClosed(reason) {
|
||||||
relayWs = null
|
relayWs = null
|
||||||
|
relayGatewayToken = ''
|
||||||
|
relayConnectRequestId = null
|
||||||
|
|
||||||
for (const [id, p] of pending.entries()) {
|
for (const [id, p] of pending.entries()) {
|
||||||
pending.delete(id)
|
pending.delete(id)
|
||||||
@@ -308,6 +316,33 @@ function sendToRelay(payload) {
|
|||||||
ws.send(JSON.stringify(payload))
|
ws.send(JSON.stringify(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureGatewayHandshakeStarted(payload) {
|
||||||
|
if (relayConnectRequestId) return
|
||||||
|
const nonce = typeof payload?.nonce === 'string' ? payload.nonce.trim() : ''
|
||||||
|
relayConnectRequestId = `ext-connect-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||||
|
sendToRelay({
|
||||||
|
type: 'req',
|
||||||
|
id: relayConnectRequestId,
|
||||||
|
method: 'connect',
|
||||||
|
params: {
|
||||||
|
minProtocol: 3,
|
||||||
|
maxProtocol: 3,
|
||||||
|
client: {
|
||||||
|
id: 'chrome-relay-extension',
|
||||||
|
version: '1.0.0',
|
||||||
|
platform: 'chrome-extension',
|
||||||
|
mode: 'webchat',
|
||||||
|
},
|
||||||
|
role: 'operator',
|
||||||
|
scopes: ['operator.read', 'operator.write'],
|
||||||
|
caps: [],
|
||||||
|
commands: [],
|
||||||
|
nonce: nonce || undefined,
|
||||||
|
auth: relayGatewayToken ? { token: relayGatewayToken } : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function maybeOpenHelpOnce() {
|
async function maybeOpenHelpOnce() {
|
||||||
try {
|
try {
|
||||||
const stored = await chrome.storage.local.get(['helpOnErrorShown'])
|
const stored = await chrome.storage.local.get(['helpOnErrorShown'])
|
||||||
@@ -349,6 +384,33 @@ async function onRelayMessage(text) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg && msg.type === 'event' && msg.event === 'connect.challenge') {
|
||||||
|
try {
|
||||||
|
ensureGatewayHandshakeStarted(msg.payload)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('gateway connect handshake start failed', err instanceof Error ? err.message : String(err))
|
||||||
|
relayConnectRequestId = null
|
||||||
|
const ws = relayWs
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close(1008, 'gateway connect failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg && msg.type === 'res' && relayConnectRequestId && msg.id === relayConnectRequestId) {
|
||||||
|
relayConnectRequestId = null
|
||||||
|
if (!msg.ok) {
|
||||||
|
const detail = msg?.error?.message || msg?.error || 'gateway connect failed'
|
||||||
|
console.warn('gateway connect handshake rejected', String(detail))
|
||||||
|
const ws = relayWs
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close(1008, 'gateway connect failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (msg && msg.method === 'ping') {
|
if (msg && msg.method === 'ping') {
|
||||||
try {
|
try {
|
||||||
sendToRelay({ method: 'pong' })
|
sendToRelay({ method: 'pong' })
|
||||||
|
|||||||
Reference in New Issue
Block a user