@qontinui/ui-bridge 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +101 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -39
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +32 -0
- package/dist/react/index.d.ts +32 -0
- package/dist/react/index.js +101 -39
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +101 -39
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.d.mts
CHANGED
|
@@ -1616,6 +1616,30 @@ interface UseCommandRelayOptions {
|
|
|
1616
1616
|
* build-time defaults baked into a static config).
|
|
1617
1617
|
*/
|
|
1618
1618
|
version?: string;
|
|
1619
|
+
/**
|
|
1620
|
+
* Optional hook that returns the current session token (the raw token
|
|
1621
|
+
* value, WITHOUT a `Bearer ` prefix). When supplied and returns a
|
|
1622
|
+
* non-empty string, the SDK:
|
|
1623
|
+
*
|
|
1624
|
+
* - attaches `Authorization: Bearer <value>` to outbound
|
|
1625
|
+
* `POST {basePath}/commands` and `POST {basePath}/heartbeat`;
|
|
1626
|
+
* - attaches `Authorization: Bearer <value>` to the SSE
|
|
1627
|
+
* `GET {basePath}/commands/stream` request as well — the stream is
|
|
1628
|
+
* consumed via `fetch` streaming, so the header rides on the request
|
|
1629
|
+
* (no token in the URL).
|
|
1630
|
+
*
|
|
1631
|
+
* Called fresh on every outbound request — implementations should read
|
|
1632
|
+
* the token from a live source (e.g. `sessionStorage`) so a token
|
|
1633
|
+
* rotation is picked up without remounting the listener. Returning
|
|
1634
|
+
* `null` / `undefined` / empty string means "no auth this call" and
|
|
1635
|
+
* the transport falls back to legacy unauth'd / cookie-based behavior
|
|
1636
|
+
* (matching the SDK's pre-`authHeader` shape).
|
|
1637
|
+
*
|
|
1638
|
+
* Required to authenticate against any relay endpoint that enforces a
|
|
1639
|
+
* session-bound auth gate (e.g. qontinui-web's
|
|
1640
|
+
* `UI_BRIDGE_REQUIRE_AUTH=1` mode).
|
|
1641
|
+
*/
|
|
1642
|
+
authHeader?: () => string | null | undefined;
|
|
1619
1643
|
}
|
|
1620
1644
|
/**
|
|
1621
1645
|
* Hook that connects the browser to the server's command relay.
|
|
@@ -1660,6 +1684,14 @@ interface CommandRelayListenerProps {
|
|
|
1660
1684
|
* defaults baked into the static config.
|
|
1661
1685
|
*/
|
|
1662
1686
|
version?: string;
|
|
1687
|
+
/**
|
|
1688
|
+
* Optional hook returning the current session token (raw value, no
|
|
1689
|
+
* `Bearer ` prefix). When supplied, the SDK attaches
|
|
1690
|
+
* `Authorization: Bearer <value>` to outbound `POST /commands` and
|
|
1691
|
+
* `POST /heartbeat` and appends `_auth=<value>` to the SSE URL.
|
|
1692
|
+
* See `UseCommandRelayOptions.authHeader` for the full contract.
|
|
1693
|
+
*/
|
|
1694
|
+
authHeader?: () => string | null | undefined;
|
|
1663
1695
|
}
|
|
1664
1696
|
declare function CommandRelayListener(props: CommandRelayListenerProps): null;
|
|
1665
1697
|
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1616,6 +1616,30 @@ interface UseCommandRelayOptions {
|
|
|
1616
1616
|
* build-time defaults baked into a static config).
|
|
1617
1617
|
*/
|
|
1618
1618
|
version?: string;
|
|
1619
|
+
/**
|
|
1620
|
+
* Optional hook that returns the current session token (the raw token
|
|
1621
|
+
* value, WITHOUT a `Bearer ` prefix). When supplied and returns a
|
|
1622
|
+
* non-empty string, the SDK:
|
|
1623
|
+
*
|
|
1624
|
+
* - attaches `Authorization: Bearer <value>` to outbound
|
|
1625
|
+
* `POST {basePath}/commands` and `POST {basePath}/heartbeat`;
|
|
1626
|
+
* - attaches `Authorization: Bearer <value>` to the SSE
|
|
1627
|
+
* `GET {basePath}/commands/stream` request as well — the stream is
|
|
1628
|
+
* consumed via `fetch` streaming, so the header rides on the request
|
|
1629
|
+
* (no token in the URL).
|
|
1630
|
+
*
|
|
1631
|
+
* Called fresh on every outbound request — implementations should read
|
|
1632
|
+
* the token from a live source (e.g. `sessionStorage`) so a token
|
|
1633
|
+
* rotation is picked up without remounting the listener. Returning
|
|
1634
|
+
* `null` / `undefined` / empty string means "no auth this call" and
|
|
1635
|
+
* the transport falls back to legacy unauth'd / cookie-based behavior
|
|
1636
|
+
* (matching the SDK's pre-`authHeader` shape).
|
|
1637
|
+
*
|
|
1638
|
+
* Required to authenticate against any relay endpoint that enforces a
|
|
1639
|
+
* session-bound auth gate (e.g. qontinui-web's
|
|
1640
|
+
* `UI_BRIDGE_REQUIRE_AUTH=1` mode).
|
|
1641
|
+
*/
|
|
1642
|
+
authHeader?: () => string | null | undefined;
|
|
1619
1643
|
}
|
|
1620
1644
|
/**
|
|
1621
1645
|
* Hook that connects the browser to the server's command relay.
|
|
@@ -1660,6 +1684,14 @@ interface CommandRelayListenerProps {
|
|
|
1660
1684
|
* defaults baked into the static config.
|
|
1661
1685
|
*/
|
|
1662
1686
|
version?: string;
|
|
1687
|
+
/**
|
|
1688
|
+
* Optional hook returning the current session token (raw value, no
|
|
1689
|
+
* `Bearer ` prefix). When supplied, the SDK attaches
|
|
1690
|
+
* `Authorization: Bearer <value>` to outbound `POST /commands` and
|
|
1691
|
+
* `POST /heartbeat` and appends `_auth=<value>` to the SSE URL.
|
|
1692
|
+
* See `UseCommandRelayOptions.authHeader` for the full contract.
|
|
1693
|
+
*/
|
|
1694
|
+
authHeader?: () => string | null | undefined;
|
|
1663
1695
|
}
|
|
1664
1696
|
declare function CommandRelayListener(props: CommandRelayListenerProps): null;
|
|
1665
1697
|
|
package/dist/react/index.js
CHANGED
|
@@ -24948,7 +24948,7 @@ function exposeProviderOnWindow(internals) {
|
|
|
24948
24948
|
if (typeof window === "undefined") return;
|
|
24949
24949
|
const w = window;
|
|
24950
24950
|
const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
|
|
24951
|
-
root.version = "0.
|
|
24951
|
+
root.version = "0.11.0";
|
|
24952
24952
|
root.specs = { getGlobalSpecStore };
|
|
24953
24953
|
root.registry = internals.registry;
|
|
24954
24954
|
root.browserCapture = internals.browserCapture;
|
|
@@ -32911,10 +32911,44 @@ function safeJsonStringify(value) {
|
|
|
32911
32911
|
});
|
|
32912
32912
|
}
|
|
32913
32913
|
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
32914
|
+
function resolveAuthToken(authHeader) {
|
|
32915
|
+
if (!authHeader) return null;
|
|
32916
|
+
try {
|
|
32917
|
+
const value = authHeader();
|
|
32918
|
+
if (typeof value !== "string") return null;
|
|
32919
|
+
const trimmed = value.trim();
|
|
32920
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
32921
|
+
} catch {
|
|
32922
|
+
return null;
|
|
32923
|
+
}
|
|
32924
|
+
}
|
|
32925
|
+
function transportHeaders(authHeader) {
|
|
32926
|
+
const headers = {
|
|
32927
|
+
"Content-Type": "application/json"
|
|
32928
|
+
};
|
|
32929
|
+
const token = resolveAuthToken(authHeader);
|
|
32930
|
+
if (token) {
|
|
32931
|
+
headers.Authorization = `Bearer ${token}`;
|
|
32932
|
+
}
|
|
32933
|
+
return headers;
|
|
32934
|
+
}
|
|
32935
|
+
function parseSSEDataBlock(block) {
|
|
32936
|
+
const dataLines = [];
|
|
32937
|
+
for (const line of block.split("\n")) {
|
|
32938
|
+
if (line.startsWith(":")) continue;
|
|
32939
|
+
if (!line.startsWith("data:")) continue;
|
|
32940
|
+
let value = line.slice(5);
|
|
32941
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
32942
|
+
dataLines.push(value);
|
|
32943
|
+
}
|
|
32944
|
+
return dataLines.length > 0 ? dataLines.join("\n") : null;
|
|
32945
|
+
}
|
|
32914
32946
|
function useCommandRelay(options) {
|
|
32915
32947
|
const enabled = options?.enabled ?? true;
|
|
32916
32948
|
const basePath = options?.basePath ?? "/api/ui-bridge";
|
|
32917
32949
|
const heartbeatIntervalMs = options?.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS;
|
|
32950
|
+
const authHeaderRef = react.useRef(void 0);
|
|
32951
|
+
authHeaderRef.current = options?.authHeader;
|
|
32918
32952
|
const uiBridge = useUIBridge();
|
|
32919
32953
|
const context = useUIBridgeOptional();
|
|
32920
32954
|
const bridge2 = react.useMemo(
|
|
@@ -32947,7 +32981,6 @@ function useCommandRelay(options) {
|
|
|
32947
32981
|
}),
|
|
32948
32982
|
[uiBridge, context]
|
|
32949
32983
|
);
|
|
32950
|
-
const eventSourceRef = react.useRef(null);
|
|
32951
32984
|
const reconnectTimeoutRef = react.useRef(null);
|
|
32952
32985
|
const processCommandRef = react.useRef(() => {
|
|
32953
32986
|
});
|
|
@@ -32970,7 +33003,7 @@ function useCommandRelay(options) {
|
|
|
32970
33003
|
try {
|
|
32971
33004
|
await fetch(`${basePath}/commands`, {
|
|
32972
33005
|
method: "POST",
|
|
32973
|
-
headers:
|
|
33006
|
+
headers: transportHeaders(authHeaderRef.current),
|
|
32974
33007
|
body: safeJsonStringify({
|
|
32975
33008
|
commandId,
|
|
32976
33009
|
success: ok,
|
|
@@ -33023,10 +33056,6 @@ function useCommandRelay(options) {
|
|
|
33023
33056
|
processCommandRef.current = processCommand;
|
|
33024
33057
|
react.useEffect(() => {
|
|
33025
33058
|
if (!enabled) {
|
|
33026
|
-
if (eventSourceRef.current) {
|
|
33027
|
-
eventSourceRef.current.close();
|
|
33028
|
-
eventSourceRef.current = null;
|
|
33029
|
-
}
|
|
33030
33059
|
if (reconnectTimeoutRef.current) {
|
|
33031
33060
|
clearTimeout(reconnectTimeoutRef.current);
|
|
33032
33061
|
reconnectTimeoutRef.current = null;
|
|
@@ -33034,35 +33063,73 @@ function useCommandRelay(options) {
|
|
|
33034
33063
|
return;
|
|
33035
33064
|
}
|
|
33036
33065
|
let isMounted = true;
|
|
33066
|
+
let abortController = null;
|
|
33067
|
+
const closeStream = () => {
|
|
33068
|
+
if (abortController) {
|
|
33069
|
+
abortController.abort();
|
|
33070
|
+
abortController = null;
|
|
33071
|
+
}
|
|
33072
|
+
};
|
|
33073
|
+
const scheduleReconnect = () => {
|
|
33074
|
+
if (!isMounted) return;
|
|
33075
|
+
if (reconnectTimeoutRef.current) return;
|
|
33076
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
33077
|
+
reconnectTimeoutRef.current = null;
|
|
33078
|
+
if (isMounted) connect();
|
|
33079
|
+
}, SSE_RECONNECT_DELAY_MS);
|
|
33080
|
+
};
|
|
33037
33081
|
const connect = () => {
|
|
33038
33082
|
if (!isMounted) return;
|
|
33039
|
-
|
|
33040
|
-
|
|
33041
|
-
|
|
33042
|
-
|
|
33043
|
-
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
|
|
33048
|
-
|
|
33049
|
-
|
|
33083
|
+
closeStream();
|
|
33084
|
+
const controller = new AbortController();
|
|
33085
|
+
abortController = controller;
|
|
33086
|
+
const url = `${basePath}/commands/stream${tabId ? `?tabId=${encodeURIComponent(tabId)}` : ""}`;
|
|
33087
|
+
const headers = { Accept: "text/event-stream" };
|
|
33088
|
+
const token = resolveAuthToken(authHeaderRef.current);
|
|
33089
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
33090
|
+
fetch(url, { method: "GET", headers, signal: controller.signal, cache: "no-store" }).then(async (resp) => {
|
|
33091
|
+
if (!resp.ok || !resp.body) {
|
|
33092
|
+
if (abortController === controller) abortController = null;
|
|
33093
|
+
scheduleReconnect();
|
|
33094
|
+
return;
|
|
33050
33095
|
}
|
|
33051
|
-
|
|
33052
|
-
|
|
33053
|
-
|
|
33054
|
-
|
|
33055
|
-
|
|
33056
|
-
|
|
33057
|
-
|
|
33058
|
-
|
|
33059
|
-
|
|
33096
|
+
const reader = resp.body.getReader();
|
|
33097
|
+
const decoder = new TextDecoder();
|
|
33098
|
+
let buffer = "";
|
|
33099
|
+
try {
|
|
33100
|
+
for (; ; ) {
|
|
33101
|
+
const { value, done } = await reader.read();
|
|
33102
|
+
if (done) break;
|
|
33103
|
+
buffer += decoder.decode(value, { stream: true });
|
|
33104
|
+
let sep;
|
|
33105
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
33106
|
+
const block = buffer.slice(0, sep);
|
|
33107
|
+
buffer = buffer.slice(sep + 2);
|
|
33108
|
+
const data = parseSSEDataBlock(block);
|
|
33109
|
+
if (data == null) continue;
|
|
33110
|
+
try {
|
|
33111
|
+
const parsed = JSON.parse(data);
|
|
33112
|
+
if (parsed.type === "connected") continue;
|
|
33113
|
+
if (parsed.commandId && parsed.action) {
|
|
33114
|
+
processCommandRef.current(parsed);
|
|
33115
|
+
}
|
|
33116
|
+
} catch (e) {
|
|
33117
|
+
console.error("[UIBridge] Failed to parse SSE message:", e);
|
|
33118
|
+
}
|
|
33119
|
+
}
|
|
33120
|
+
}
|
|
33121
|
+
} catch {
|
|
33060
33122
|
}
|
|
33061
|
-
|
|
33123
|
+
if (abortController === controller) abortController = null;
|
|
33124
|
+
if (isMounted && !controller.signal.aborted) scheduleReconnect();
|
|
33125
|
+
}).catch(() => {
|
|
33126
|
+
if (abortController === controller) abortController = null;
|
|
33127
|
+
if (isMounted && !controller.signal.aborted) scheduleReconnect();
|
|
33128
|
+
});
|
|
33062
33129
|
};
|
|
33063
33130
|
const handleVisibility = () => {
|
|
33064
33131
|
if (document.visibilityState === "visible") {
|
|
33065
|
-
if (!
|
|
33132
|
+
if (!abortController) {
|
|
33066
33133
|
if (reconnectTimeoutRef.current) {
|
|
33067
33134
|
clearTimeout(reconnectTimeoutRef.current);
|
|
33068
33135
|
reconnectTimeoutRef.current = null;
|
|
@@ -33075,10 +33142,7 @@ function useCommandRelay(options) {
|
|
|
33075
33142
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
33076
33143
|
forceReconnectRef.current = () => {
|
|
33077
33144
|
if (!isMounted) return;
|
|
33078
|
-
|
|
33079
|
-
eventSourceRef.current.close();
|
|
33080
|
-
eventSourceRef.current = null;
|
|
33081
|
-
}
|
|
33145
|
+
closeStream();
|
|
33082
33146
|
if (reconnectTimeoutRef.current) {
|
|
33083
33147
|
clearTimeout(reconnectTimeoutRef.current);
|
|
33084
33148
|
reconnectTimeoutRef.current = null;
|
|
@@ -33089,10 +33153,7 @@ function useCommandRelay(options) {
|
|
|
33089
33153
|
isMounted = false;
|
|
33090
33154
|
forceReconnectRef.current = () => {
|
|
33091
33155
|
};
|
|
33092
|
-
|
|
33093
|
-
eventSourceRef.current.close();
|
|
33094
|
-
eventSourceRef.current = null;
|
|
33095
|
-
}
|
|
33156
|
+
closeStream();
|
|
33096
33157
|
if (reconnectTimeoutRef.current) {
|
|
33097
33158
|
clearTimeout(reconnectTimeoutRef.current);
|
|
33098
33159
|
reconnectTimeoutRef.current = null;
|
|
@@ -33125,7 +33186,7 @@ function useCommandRelay(options) {
|
|
|
33125
33186
|
if (heartbeatVersion !== void 0) body.version = heartbeatVersion;
|
|
33126
33187
|
const resp = await fetch(`${basePath}/heartbeat`, {
|
|
33127
33188
|
method: "POST",
|
|
33128
|
-
headers:
|
|
33189
|
+
headers: transportHeaders(authHeaderRef.current),
|
|
33129
33190
|
body: JSON.stringify(body)
|
|
33130
33191
|
});
|
|
33131
33192
|
if (resp.ok) {
|
|
@@ -33256,7 +33317,8 @@ function CommandRelayListener(props) {
|
|
|
33256
33317
|
appType: props.appType,
|
|
33257
33318
|
framework: props.framework,
|
|
33258
33319
|
capabilities: props.capabilities,
|
|
33259
|
-
version: props.version
|
|
33320
|
+
version: props.version,
|
|
33321
|
+
authHeader: props.authHeader
|
|
33260
33322
|
};
|
|
33261
33323
|
useCommandRelay(options);
|
|
33262
33324
|
return null;
|