@qontinui/ui-bridge 0.21.0 → 0.21.1

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 CHANGED
@@ -26949,7 +26949,7 @@ function exposeProviderOnWindow(internals) {
26949
26949
  if (typeof window === "undefined") return;
26950
26950
  const w = window;
26951
26951
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
26952
- root.version = "0.21.0";
26952
+ root.version = "0.21.1";
26953
26953
  root.specs = { getGlobalSpecStore };
26954
26954
  root.registry = internals.registry;
26955
26955
  root.browserCapture = internals.browserCapture;
package/dist/index.mjs CHANGED
@@ -26947,7 +26947,7 @@ function exposeProviderOnWindow(internals) {
26947
26947
  if (typeof window === "undefined") return;
26948
26948
  const w = window;
26949
26949
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
26950
- root.version = "0.21.0";
26950
+ root.version = "0.21.1";
26951
26951
  root.specs = { getGlobalSpecStore };
26952
26952
  root.registry = internals.registry;
26953
26953
  root.browserCapture = internals.browserCapture;
@@ -31,5 +31,5 @@ ${l(f+p,a-30)}`);}if(e.contentChanges&&a>30){let{metricChanges:m,statusChanges:f
31
31
  `)){if(t.startsWith(":")||!t.startsWith("data:"))continue;let r=t.slice(5);r.startsWith(" ")&&(r=r.slice(1)),e.push(r);}return e.length>0?e.join(`
32
32
  `):null}function cd(n){if(!n)return null;try{let e=n();if(!e||typeof e!="object")return null;let t=e.userId,r=e.sessionId;return typeof t!="string"||t.trim().length===0||typeof r!="string"||r.trim().length===0?null:{userId:t.trim(),sessionId:r.trim()}}catch{return null}}function Ci(n){try{typeof sessionStorage<"u"&&sessionStorage.setItem(Sa,n);}catch{}}function ld(){if(typeof window>"u")return null;try{let n=window,e=n.__uiBridgeTabId;if(typeof e=="string"&&e.length>0)return e;let t=n.__uiBridgeInjectedConfig?.tabId;if(typeof t=="string"&&t.length>0)return t}catch{}return null}function ud(){let n=ld();if(n)return Ci(n),n;let e=null;try{typeof sessionStorage<"u"&&(e=sessionStorage.getItem(Sa));}catch{}if(e)return e;let t=crypto.randomUUID();return Ci(t),t}function Ta(n){let{basePath:e,execute:t,heartbeatIntervalMs:r=1e4,authHeader:i,registrationMetadata:s,appType:o,capabilities:a,appId:c,appName:l,framework:u,version:d,onError:m=(A,M)=>console.error(A,M)}=n,f=n.tabId??ud();n.tabId&&Ci(n.tabId);let p=false,h=null,y=null,b=null,E=async(A,M,T)=>{try{await fetch(`${e}/commands`,{method:"POST",headers:wa(i),body:od({commandId:A,success:M,result:T,tabId:f,error:M?void 0:T?.error})});}catch(N){m("[UIBridge] Failed to send command response:",N);}},v=async A=>{try{let M=await t(A.action,A.payload),T=M;if(T&&T.success===!1){await E(A.commandId,!1,M);return}await E(A.commandId,!0,M);}catch(M){let T=M instanceof Error?M.message:String(M),N="UNKNOWN_ERROR";T.includes("not found")?N="ELEMENT_NOT_FOUND":T.includes("timeout")?N="ACTION_TIMEOUT":T.includes("Unknown command")&&(N="UNKNOWN_COMMAND"),await E(A.commandId,false,{success:false,error:T,failureDetails:{errorCode:N,message:T,timestamp:Date.now()},durationMs:0,timestamp:Date.now()});}},w=()=>{h&&(h.abort(),h=null);},R=()=>{p||y||(y=setTimeout(()=>{y=null,p||L();},1e4));},L=()=>{if(p)return;w();let A=new AbortController;h=A;let M=`${e}/commands/stream${f?`?tabId=${encodeURIComponent(f)}`:""}`,T={Accept:"text/event-stream"},N=Ca(i);N&&(T.Authorization=`Bearer ${N}`),fetch(M,{method:"GET",headers:T,signal:A.signal,cache:"no-store"}).then(async I=>{if(!I.ok||!I.body){h===A&&(h=null),R();return}let V=I.body.getReader(),Q=new TextDecoder,W="";try{for(;;){let{value:de,done:Ce}=await V.read();if(Ce)break;W+=Q.decode(de,{stream:!0});let Te;for(;(Te=W.indexOf(`
33
33
 
34
- `))!==-1;){let Ct=W.slice(0,Te);W=W.slice(Te+2);let Ge=ad(Ct);if(Ge!=null)try{let ee=JSON.parse(Ge);if(ee.type==="connected")continue;ee.commandId&&ee.action&&v(ee);}catch(ee){m("[UIBridge] Failed to parse SSE message:",ee);}}}}catch{}h===A&&(h=null),!p&&!A.signal.aborted&&R();}).catch(()=>{h===A&&(h=null),!p&&!A.signal.aborted&&R();});},x=()=>{p||(w(),y&&(clearTimeout(y),y=null),L());},O=()=>{typeof document>"u"||document.visibilityState==="visible"&&(h||(y&&(clearTimeout(y),y=null),L()));},k=async()=>{try{let A={timestamp:Date.now(),tabId:f,url:typeof window<"u"?window.location.href:void 0,title:typeof document<"u"?document.title:void 0,visibility:typeof document<"u"?document.visibilityState:void 0};c!==void 0&&(A.appId=c),l!==void 0&&(A.appName=l),o!==void 0&&(A.appType=o),u!==void 0&&(A.framework=u),a!==void 0&&(A.capabilities=a),d!==void 0&&(A.version=d);let M=cd(s);M!==null&&(A.registrationMetadata=M);let T=await fetch(`${e}/heartbeat`,{method:"POST",headers:wa(i),body:JSON.stringify(A)});if(T.ok)try{let N=await T.json();(N?.tabRegistered??N?.data?.tabRegistered??null)===!1&&x();}catch{}}catch{}};return L(),typeof document<"u"&&document.addEventListener("visibilitychange",O),k(),b=setInterval(()=>{k();},r),{tabId:f,forceReconnect:x,stop(){p||(p=true,w(),y&&(clearTimeout(y),y=null),b&&(clearInterval(b),b=null),typeof document<"u"&&document.removeEventListener("visibilitychange",O));}}}function dd(n){return n.element.isConnected}function fd(n,e,t={}){let r=t.tracked,i=Ws(e),s=new Set(n.getAllElements().map(a=>a.id)),o=0;for(let a of i){if(r?.has(a.element)||!dd(a))continue;let c=a.id;if(s.has(c)){let u=2;for(;s.has(`${c}-${u}`);)u++;c=`${c}-${u}`;}let l=n.registerElement(c,a.element,{label:a.label||void 0,origin:"auto"});s.add(l.id),r?.set(a.element,l.id),o++;}return {registered:o,total:i.length}}function xa(n,e=document.body,t={}){let r=new Map,i=t.debounceMs??100,s=t.onSeed,o=null,a=()=>{let u=fd(n,e,{tracked:r});return s?.(n.getAllElements().length),u};a();let c=u=>{let d=r.get(u);d&&!u.isConnected&&(n.unregisterElement(d),r.delete(u));},l=new MutationObserver(u=>{for(let d of u)d.removedNodes.forEach(m=>{if(m.nodeType!==Node.ELEMENT_NODE)return;let f=m;c(f),f.querySelectorAll("*").forEach(c);});o&&clearTimeout(o),o=setTimeout(a,i);});return l.observe(e,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","hidden","disabled","aria-hidden"]}),()=>{l.disconnect(),o&&(clearTimeout(o),o=null);}}function Aa(n){let{quietMs:e,timeoutMs:t}=n,r=false,i=false,s=0,o=false,a=null,c=null,l=[],u=()=>({settled:r,elementCount:s,settledByTimeout:i,expectSatisfied:o}),d=()=>{a&&(clearTimeout(a),a=null);},m=()=>{c&&(clearTimeout(c),c=null);},f=p=>{if(r)return;r=true,i=p,d(),m();let h=u();for(;l.length;)l.shift()?.(h);};return {get settled(){return r},get elementCount(){return s},get settledByTimeout(){return i},get expectSatisfied(){return o},start(){r||c||(c=setTimeout(()=>f(true),t));},noteSeed(p,h){s=p,o=h??p>0,!r&&(o?(d(),a=setTimeout(()=>f(false),e)):d());},whenSettled(p){return r?Promise.resolve(u()):new Promise(h=>{let y=false,b=v=>{y||(y=true,h(v));};l.push(b),setTimeout(()=>b(u()),typeof p=="number"?p:t);})},dispose(){d(),m();}}}function Ra(n){return {get elements(){return n.getAllElements()},getElement:e=>n.getElement(e),get components(){return n.getAllComponents()},get workflows(){return n.getAllWorkflows()}}}(function(){if(typeof window>"u"||window.__uiBridgeInjected)return;let e=new Je;ps(e);let t=Ra(e),r=window.__uiBridgeInjectedConfig,i=Aa({quietMs:typeof r?.settleQuietMs=="number"?r.settleQuietMs:500,timeoutMs:typeof r?.settleTimeoutMs=="number"?r.settleTimeoutMs:1e4}),s=typeof r?.expectSelector=="string"&&r.expectSelector.length>0?r.expectSelector:null,o=()=>{if(!s)return true;try{return document.querySelector(s)!==null}catch{return true}},a=()=>{c.settled=i.settled,c.settledByTimeout=i.settledByTimeout,c.expectSatisfied=i.expectSatisfied;},c={ready:false,settled:false,settledByTimeout:false,expectSatisfied:false,expectSelector:s,elementCount:0,version:"0.21.0",whenSettled:u=>i.whenSettled(u),execute:(u,d)=>Promise.resolve(Hn(u,d,t))};window.__uiBridgeInjected=c;let l=()=>{i.start(),xa(e,document.body,{onSeed:u=>{c.elementCount=u,i.noteSeed(u,s?o():void 0),a();}}),c.ready=true,i.whenSettled().then(a),r?.uiBridgeBase&&Ta({basePath:r.uiBridgeBase,execute:c.execute,tabId:r.tabId,authHeader:()=>r.authToken??null,registrationMetadata:()=>r.registrationMetadata??null,appType:"injected",capabilities:["control","discovery"],appId:r.appId,appName:r.appName,version:c.version});};document.body?l():typeof document<"u"&&document.addEventListener("DOMContentLoaded",l,{once:true});})();})();//# sourceMappingURL=bundle.global.js.map
34
+ `))!==-1;){let Ct=W.slice(0,Te);W=W.slice(Te+2);let Ge=ad(Ct);if(Ge!=null)try{let ee=JSON.parse(Ge);if(ee.type==="connected")continue;ee.commandId&&ee.action&&v(ee);}catch(ee){m("[UIBridge] Failed to parse SSE message:",ee);}}}}catch{}h===A&&(h=null),!p&&!A.signal.aborted&&R();}).catch(()=>{h===A&&(h=null),!p&&!A.signal.aborted&&R();});},x=()=>{p||(w(),y&&(clearTimeout(y),y=null),L());},O=()=>{typeof document>"u"||document.visibilityState==="visible"&&(h||(y&&(clearTimeout(y),y=null),L()));},k=async()=>{try{let A={timestamp:Date.now(),tabId:f,url:typeof window<"u"?window.location.href:void 0,title:typeof document<"u"?document.title:void 0,visibility:typeof document<"u"?document.visibilityState:void 0};c!==void 0&&(A.appId=c),l!==void 0&&(A.appName=l),o!==void 0&&(A.appType=o),u!==void 0&&(A.framework=u),a!==void 0&&(A.capabilities=a),d!==void 0&&(A.version=d);let M=cd(s);M!==null&&(A.registrationMetadata=M);let T=await fetch(`${e}/heartbeat`,{method:"POST",headers:wa(i),body:JSON.stringify(A)});if(T.ok)try{let N=await T.json();(N?.tabRegistered??N?.data?.tabRegistered??null)===!1&&x();}catch{}}catch{}};return L(),typeof document<"u"&&document.addEventListener("visibilitychange",O),k(),b=setInterval(()=>{k();},r),{tabId:f,forceReconnect:x,stop(){p||(p=true,w(),y&&(clearTimeout(y),y=null),b&&(clearInterval(b),b=null),typeof document<"u"&&document.removeEventListener("visibilitychange",O));}}}function dd(n){return n.element.isConnected}function fd(n,e,t={}){let r=t.tracked,i=Ws(e),s=new Set(n.getAllElements().map(a=>a.id)),o=0;for(let a of i){if(r?.has(a.element)||!dd(a))continue;let c=a.id;if(s.has(c)){let u=2;for(;s.has(`${c}-${u}`);)u++;c=`${c}-${u}`;}let l=n.registerElement(c,a.element,{label:a.label||void 0,origin:"auto"});s.add(l.id),r?.set(a.element,l.id),o++;}return {registered:o,total:i.length}}function xa(n,e=document.body,t={}){let r=new Map,i=t.debounceMs??100,s=t.onSeed,o=null,a=()=>{let u=fd(n,e,{tracked:r});return s?.(n.getAllElements().length),u};a();let c=u=>{let d=r.get(u);d&&!u.isConnected&&(n.unregisterElement(d),r.delete(u));},l=new MutationObserver(u=>{for(let d of u)d.removedNodes.forEach(m=>{if(m.nodeType!==Node.ELEMENT_NODE)return;let f=m;c(f),f.querySelectorAll("*").forEach(c);});o&&clearTimeout(o),o=setTimeout(a,i);});return l.observe(e,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","hidden","disabled","aria-hidden"]}),()=>{l.disconnect(),o&&(clearTimeout(o),o=null);}}function Aa(n){let{quietMs:e,timeoutMs:t}=n,r=false,i=false,s=0,o=false,a=null,c=null,l=[],u=()=>({settled:r,elementCount:s,settledByTimeout:i,expectSatisfied:o}),d=()=>{a&&(clearTimeout(a),a=null);},m=()=>{c&&(clearTimeout(c),c=null);},f=p=>{if(r)return;r=true,i=p,d(),m();let h=u();for(;l.length;)l.shift()?.(h);};return {get settled(){return r},get elementCount(){return s},get settledByTimeout(){return i},get expectSatisfied(){return o},start(){r||c||(c=setTimeout(()=>f(true),t));},noteSeed(p,h){s=p,o=h??p>0,!r&&(o?(d(),a=setTimeout(()=>f(false),e)):d());},whenSettled(p){return r?Promise.resolve(u()):new Promise(h=>{let y=false,b=v=>{y||(y=true,h(v));};l.push(b),setTimeout(()=>b(u()),typeof p=="number"?p:t);})},dispose(){d(),m();}}}function Ra(n){return {get elements(){return n.getAllElements()},getElement:e=>n.getElement(e),get components(){return n.getAllComponents()},get workflows(){return n.getAllWorkflows()}}}(function(){if(typeof window>"u"||window.__uiBridgeInjected)return;let e=new Je;ps(e);let t=Ra(e),r=window.__uiBridgeInjectedConfig,i=Aa({quietMs:typeof r?.settleQuietMs=="number"?r.settleQuietMs:500,timeoutMs:typeof r?.settleTimeoutMs=="number"?r.settleTimeoutMs:1e4}),s=typeof r?.expectSelector=="string"&&r.expectSelector.length>0?r.expectSelector:null,o=()=>{if(!s)return true;try{return document.querySelector(s)!==null}catch{return true}},a=()=>{c.settled=i.settled,c.settledByTimeout=i.settledByTimeout,c.expectSatisfied=i.expectSatisfied;},c={ready:false,settled:false,settledByTimeout:false,expectSatisfied:false,expectSelector:s,elementCount:0,version:"0.21.1",whenSettled:u=>i.whenSettled(u),execute:(u,d)=>Promise.resolve(Hn(u,d,t))};window.__uiBridgeInjected=c;let l=()=>{i.start(),xa(e,document.body,{onSeed:u=>{c.elementCount=u,i.noteSeed(u,s?o():void 0),a();}}),c.ready=true,i.whenSettled().then(a),r?.uiBridgeBase&&Ta({basePath:r.uiBridgeBase,execute:c.execute,tabId:r.tabId,authHeader:()=>r.authToken??null,registrationMetadata:()=>r.registrationMetadata??null,appType:"injected",capabilities:["control","discovery"],appId:r.appId,appName:r.appName,version:c.version});};document.body?l():typeof document<"u"&&document.addEventListener("DOMContentLoaded",l,{once:true});})();})();//# sourceMappingURL=bundle.global.js.map
35
35
  //# sourceMappingURL=bundle.global.js.map
@@ -495,17 +495,45 @@ declare class CommandRelay {
495
495
  * has been called for it. `listOwnedTabs`, `assertOwnership`, and the
496
496
  * bus-envelope receiver check all key off this map.
497
497
  *
498
- * Re-registration under a DIFFERENT `userId` is permitted (e.g. a tab
499
- * survives a re-login) — the entry is overwritten in place. This is
500
- * the safe path: the alternative (reject + force a reconnect) would
501
- * leave the tab unaddressable until the SSE stream re-establishes,
502
- * which is strictly worse for the operator UX. The new ownership
503
- * takes effect on the next dispatch.
498
+ * Re-registration under a DIFFERENT `userId` (e.g. a tab survives a
499
+ * re-login) is permitted ONLY when the new user is AUTH-PROVEN, not
500
+ * merely asserted. `metadata.userId` rides in the (browser-supplied)
501
+ * heartbeat body and is therefore untrusted on its own: without a
502
+ * proof gate, anyone who learns a victim's `tabId` could POST a
503
+ * heartbeat claiming any `userId` and silently seize ownership of the
504
+ * tab (and with it the ability to drive it / settle its commands).
505
+ *
506
+ * `options.callerUserId` is the identity the consumer's auth gate
507
+ * proved and injected as `X-Caller-User-Id` (never a browser value).
508
+ * The rule:
509
+ *
510
+ * - Same-owner heartbeat (existing owner === claimed userId): always
511
+ * a refresh — `sessionId`/`lastSeen` updated in place. No proof
512
+ * needed; you already hold the tab and the claim matches.
513
+ * - TRANSFER (a DIFFERENT user already owns the tab): the claimed
514
+ * `userId` MUST be auth-proven and equal to `callerUserId`. An
515
+ * unauthenticated (`callerUserId` absent) or mismatched claim is
516
+ * REFUSED and the prior owner is left untouched — this is the
517
+ * takeover primitive being closed. A genuine re-login carries the
518
+ * new user's proven identity and therefore still transfers.
519
+ * - First CLAIM (tab has no owner yet — bootstrap): honored, so a
520
+ * header-less / single-trust-domain deployment can still register
521
+ * tabs. A *present* proven identity must still match the claimed
522
+ * `userId` (you cannot bootstrap a tab under an identity you
523
+ * cannot back).
524
+ *
525
+ * Returns whether ownership was (re)assigned so the HTTP layer can
526
+ * surface the refusal without bricking the heartbeat loop.
504
527
  */
505
528
  recordRegistration(tabId: string, metadata: {
506
529
  userId: string;
507
530
  sessionId: string;
508
- }): void;
531
+ }, options?: {
532
+ callerUserId?: string | null;
533
+ }): {
534
+ ownershipAssigned: boolean;
535
+ rejectedReason?: 'OWNER_CHANGE_UNVERIFIED';
536
+ };
509
537
  /**
510
538
  * Return the stored ownership record for a tab, or `null` if the tab
511
539
  * has not registered yet. The full record (including timestamps) is
@@ -495,17 +495,45 @@ declare class CommandRelay {
495
495
  * has been called for it. `listOwnedTabs`, `assertOwnership`, and the
496
496
  * bus-envelope receiver check all key off this map.
497
497
  *
498
- * Re-registration under a DIFFERENT `userId` is permitted (e.g. a tab
499
- * survives a re-login) — the entry is overwritten in place. This is
500
- * the safe path: the alternative (reject + force a reconnect) would
501
- * leave the tab unaddressable until the SSE stream re-establishes,
502
- * which is strictly worse for the operator UX. The new ownership
503
- * takes effect on the next dispatch.
498
+ * Re-registration under a DIFFERENT `userId` (e.g. a tab survives a
499
+ * re-login) is permitted ONLY when the new user is AUTH-PROVEN, not
500
+ * merely asserted. `metadata.userId` rides in the (browser-supplied)
501
+ * heartbeat body and is therefore untrusted on its own: without a
502
+ * proof gate, anyone who learns a victim's `tabId` could POST a
503
+ * heartbeat claiming any `userId` and silently seize ownership of the
504
+ * tab (and with it the ability to drive it / settle its commands).
505
+ *
506
+ * `options.callerUserId` is the identity the consumer's auth gate
507
+ * proved and injected as `X-Caller-User-Id` (never a browser value).
508
+ * The rule:
509
+ *
510
+ * - Same-owner heartbeat (existing owner === claimed userId): always
511
+ * a refresh — `sessionId`/`lastSeen` updated in place. No proof
512
+ * needed; you already hold the tab and the claim matches.
513
+ * - TRANSFER (a DIFFERENT user already owns the tab): the claimed
514
+ * `userId` MUST be auth-proven and equal to `callerUserId`. An
515
+ * unauthenticated (`callerUserId` absent) or mismatched claim is
516
+ * REFUSED and the prior owner is left untouched — this is the
517
+ * takeover primitive being closed. A genuine re-login carries the
518
+ * new user's proven identity and therefore still transfers.
519
+ * - First CLAIM (tab has no owner yet — bootstrap): honored, so a
520
+ * header-less / single-trust-domain deployment can still register
521
+ * tabs. A *present* proven identity must still match the claimed
522
+ * `userId` (you cannot bootstrap a tab under an identity you
523
+ * cannot back).
524
+ *
525
+ * Returns whether ownership was (re)assigned so the HTTP layer can
526
+ * surface the refusal without bricking the heartbeat loop.
504
527
  */
505
528
  recordRegistration(tabId: string, metadata: {
506
529
  userId: string;
507
530
  sessionId: string;
508
- }): void;
531
+ }, options?: {
532
+ callerUserId?: string | null;
533
+ }): {
534
+ ownershipAssigned: boolean;
535
+ rejectedReason?: 'OWNER_CHANGE_UNVERIFIED';
536
+ };
509
537
  /**
510
538
  * Return the stored ownership record for a tab, or `null` if the tab
511
539
  * has not registered yet. The full record (including timestamps) is
@@ -26035,7 +26035,7 @@ function exposeProviderOnWindow(internals) {
26035
26035
  if (typeof window === "undefined") return;
26036
26036
  const w = window;
26037
26037
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
26038
- root.version = "0.21.0";
26038
+ root.version = "0.21.1";
26039
26039
  root.specs = { getGlobalSpecStore };
26040
26040
  root.registry = internals.registry;
26041
26041
  root.browserCapture = internals.browserCapture;
@@ -26033,7 +26033,7 @@ function exposeProviderOnWindow(internals) {
26033
26033
  if (typeof window === "undefined") return;
26034
26034
  const w = window;
26035
26035
  const root = w.__UI_BRIDGE__ ?? (w.__UI_BRIDGE__ = {});
26036
- root.version = "0.21.0";
26036
+ root.version = "0.21.1";
26037
26037
  root.specs = { getGlobalSpecStore };
26038
26038
  root.registry = internals.registry;
26039
26039
  root.browserCapture = internals.browserCapture;
@@ -2,8 +2,8 @@ import { a as UIBridgeServerHandlers, H as HandlerContext } from '../types-DHctE
2
2
  export { A as APIResponse, B as BrowserEventsResponse, C as CORSOptions, b as CapabilitiesResponse, c as ControlBatchRequest, d as ControlBatchResponse, e as ControlBatchStep, f as ControlBatchStepResult, D as DOMChangeEvent, E as ElementConditionSelector, g as ElementExpectRequest, h as ElementExpectResponse, i as EndpointCategory, j as EndpointInfo, I as INTERACTION_RELAY_COMMAND_ACTIONS, k as InteractionRelayCommandAction, P as PageEvaluateRequest, R as RateLimitOptions, l as RenderLogQuery, m as RouteDefinition, n as RunnerWindow, o as RunnerWindowsList, S as SpawnHeadlessRequest, p as SpawnHeadlessResponse, q as StateSummary, U as UIBridgeServerConfig, r as UI_BRIDGE_ROUTES, s as WaitForElementByConditionRequest, t as WaitForElementByConditionResponse, u as WaitForElementPredicate, v as WaitForElementRequest, w as WaitForElementSuccessResponse, x as WaitForRouteChangeRequest, y as WaitForRouteChangeResponse, W as WebSocketMessage, z as WebSocketMessageType } from '../types-DHctEsDy.mjs';
3
3
  export { ActionExecutorLike, CreateHandlersConfig, RegistryLike, closeAllSpawnedHeadlessTabs, createAIHandlers, createHandlers } from './handlers.mjs';
4
4
  export { ExpressAdapterConfig, createExpressApp, createExpressRouter, uiBridgeMiddleware } from './express.mjs';
5
- import { C as CommandRelay } from '../nextjs-DZf4_LXo.mjs';
6
- export { a as CommandListener, b as CommandRelayOptions, N as NextJSAdapterConfig, c as NextRouteHandler, P as PendingCommand, Q as QueuedCommand, R as RedisRelayBus, d as RelayBus, e as RelayCommandEnvelope, f as RelayResponseEnvelope, T as TabInfo, g as TabListener, h as TabRoutingError, i as TransportDiagnostics, W as WebSocketClient, j as createControlHandlers, k as createDebugHandlers, l as createNextRouteHandlers, m as createRedisRelayBus, n as createRenderLogHandlers, o as createUIBridgeHandler } from '../nextjs-DZf4_LXo.mjs';
5
+ import { C as CommandRelay } from '../nextjs-DCLP7WgI.mjs';
6
+ export { a as CommandListener, b as CommandRelayOptions, N as NextJSAdapterConfig, c as NextRouteHandler, P as PendingCommand, Q as QueuedCommand, R as RedisRelayBus, d as RelayBus, e as RelayCommandEnvelope, f as RelayResponseEnvelope, T as TabInfo, g as TabListener, h as TabRoutingError, i as TransportDiagnostics, W as WebSocketClient, j as createControlHandlers, k as createDebugHandlers, l as createNextRouteHandlers, m as createRedisRelayBus, n as createRenderLogHandlers, o as createUIBridgeHandler } from '../nextjs-DCLP7WgI.mjs';
7
7
  import { U as UIBridgeWSHandler } from '../standalone-Bz312WSC.mjs';
8
8
  export { S as StandaloneServer, a as StandaloneServerConfig, W as WebSocketLike, c as createStandaloneServer, s as startCLI } from '../standalone-Bz312WSC.mjs';
9
9
  import { e as BridgeEvent, i as AnyCapturedEvent, Q as ControlActionRequest } from '../types-CDn2gSl_.mjs';
@@ -2,8 +2,8 @@ import { a as UIBridgeServerHandlers, H as HandlerContext } from '../types-DRCea
2
2
  export { A as APIResponse, B as BrowserEventsResponse, C as CORSOptions, b as CapabilitiesResponse, c as ControlBatchRequest, d as ControlBatchResponse, e as ControlBatchStep, f as ControlBatchStepResult, D as DOMChangeEvent, E as ElementConditionSelector, g as ElementExpectRequest, h as ElementExpectResponse, i as EndpointCategory, j as EndpointInfo, I as INTERACTION_RELAY_COMMAND_ACTIONS, k as InteractionRelayCommandAction, P as PageEvaluateRequest, R as RateLimitOptions, l as RenderLogQuery, m as RouteDefinition, n as RunnerWindow, o as RunnerWindowsList, S as SpawnHeadlessRequest, p as SpawnHeadlessResponse, q as StateSummary, U as UIBridgeServerConfig, r as UI_BRIDGE_ROUTES, s as WaitForElementByConditionRequest, t as WaitForElementByConditionResponse, u as WaitForElementPredicate, v as WaitForElementRequest, w as WaitForElementSuccessResponse, x as WaitForRouteChangeRequest, y as WaitForRouteChangeResponse, W as WebSocketMessage, z as WebSocketMessageType } from '../types-DRCeaV6_.js';
3
3
  export { ActionExecutorLike, CreateHandlersConfig, RegistryLike, closeAllSpawnedHeadlessTabs, createAIHandlers, createHandlers } from './handlers.js';
4
4
  export { ExpressAdapterConfig, createExpressApp, createExpressRouter, uiBridgeMiddleware } from './express.js';
5
- import { C as CommandRelay } from '../nextjs-NxTdtHhA.js';
6
- export { a as CommandListener, b as CommandRelayOptions, N as NextJSAdapterConfig, c as NextRouteHandler, P as PendingCommand, Q as QueuedCommand, R as RedisRelayBus, d as RelayBus, e as RelayCommandEnvelope, f as RelayResponseEnvelope, T as TabInfo, g as TabListener, h as TabRoutingError, i as TransportDiagnostics, W as WebSocketClient, j as createControlHandlers, k as createDebugHandlers, l as createNextRouteHandlers, m as createRedisRelayBus, n as createRenderLogHandlers, o as createUIBridgeHandler } from '../nextjs-NxTdtHhA.js';
5
+ import { C as CommandRelay } from '../nextjs-Bu7ZGwGe.js';
6
+ export { a as CommandListener, b as CommandRelayOptions, N as NextJSAdapterConfig, c as NextRouteHandler, P as PendingCommand, Q as QueuedCommand, R as RedisRelayBus, d as RelayBus, e as RelayCommandEnvelope, f as RelayResponseEnvelope, T as TabInfo, g as TabListener, h as TabRoutingError, i as TransportDiagnostics, W as WebSocketClient, j as createControlHandlers, k as createDebugHandlers, l as createNextRouteHandlers, m as createRedisRelayBus, n as createRenderLogHandlers, o as createUIBridgeHandler } from '../nextjs-Bu7ZGwGe.js';
7
7
  import { U as UIBridgeWSHandler } from '../standalone-Q5b8CZwc.js';
8
8
  export { S as StandaloneServer, a as StandaloneServerConfig, W as WebSocketLike, c as createStandaloneServer, s as startCLI } from '../standalone-Q5b8CZwc.js';
9
9
  import { e as BridgeEvent, i as AnyCapturedEvent, Q as ControlActionRequest } from '../types-DJO1izBV.js';
@@ -25867,7 +25867,12 @@ function handleRelayRoute(method, path, request, relay, config) {
25867
25867
  400
25868
25868
  );
25869
25869
  }
25870
- relay.recordRegistration(heartbeatTabId, { userId, sessionId });
25870
+ const callerUserId = request.headers.get("x-caller-user-id") ?? request.headers.get("X-Caller-User-Id") ?? void 0;
25871
+ const registration = relay.recordRegistration(
25872
+ heartbeatTabId,
25873
+ { userId, sessionId },
25874
+ { callerUserId }
25875
+ );
25871
25876
  relay.receiveHeartbeat(heartbeatTabId, {
25872
25877
  url: typeof body?.url === "string" ? body.url : void 0,
25873
25878
  title: typeof body?.title === "string" ? body.title : void 0,
@@ -25877,7 +25882,15 @@ function handleRelayRoute(method, path, request, relay, config) {
25877
25882
  const tabRegistered = diag.connectedTabs.includes(heartbeatTabId);
25878
25883
  return jsonResponse({
25879
25884
  success: true,
25880
- data: { received: true, tabRegistered },
25885
+ data: {
25886
+ received: true,
25887
+ tabRegistered,
25888
+ // Observability: signals an ownership claim/transfer was refused
25889
+ // because the claimed userId was not auth-proven. Not a fatal
25890
+ // heartbeat error — the loop continues; the tab simply keeps its
25891
+ // prior (or no) owner. Absent when ownership was assigned.
25892
+ ...registration.ownershipAssigned ? {} : { ownershipAssigned: false, ownerChangeRejected: registration.rejectedReason }
25893
+ },
25881
25894
  timestamp: Date.now()
25882
25895
  });
25883
25896
  })();
@@ -29127,20 +29140,52 @@ var CommandRelay = class {
29127
29140
  * has been called for it. `listOwnedTabs`, `assertOwnership`, and the
29128
29141
  * bus-envelope receiver check all key off this map.
29129
29142
  *
29130
- * Re-registration under a DIFFERENT `userId` is permitted (e.g. a tab
29131
- * survives a re-login) — the entry is overwritten in place. This is
29132
- * the safe path: the alternative (reject + force a reconnect) would
29133
- * leave the tab unaddressable until the SSE stream re-establishes,
29134
- * which is strictly worse for the operator UX. The new ownership
29135
- * takes effect on the next dispatch.
29136
- */
29137
- recordRegistration(tabId, metadata) {
29143
+ * Re-registration under a DIFFERENT `userId` (e.g. a tab survives a
29144
+ * re-login) is permitted ONLY when the new user is AUTH-PROVEN, not
29145
+ * merely asserted. `metadata.userId` rides in the (browser-supplied)
29146
+ * heartbeat body and is therefore untrusted on its own: without a
29147
+ * proof gate, anyone who learns a victim's `tabId` could POST a
29148
+ * heartbeat claiming any `userId` and silently seize ownership of the
29149
+ * tab (and with it the ability to drive it / settle its commands).
29150
+ *
29151
+ * `options.callerUserId` is the identity the consumer's auth gate
29152
+ * proved and injected as `X-Caller-User-Id` (never a browser value).
29153
+ * The rule:
29154
+ *
29155
+ * - Same-owner heartbeat (existing owner === claimed userId): always
29156
+ * a refresh — `sessionId`/`lastSeen` updated in place. No proof
29157
+ * needed; you already hold the tab and the claim matches.
29158
+ * - TRANSFER (a DIFFERENT user already owns the tab): the claimed
29159
+ * `userId` MUST be auth-proven and equal to `callerUserId`. An
29160
+ * unauthenticated (`callerUserId` absent) or mismatched claim is
29161
+ * REFUSED and the prior owner is left untouched — this is the
29162
+ * takeover primitive being closed. A genuine re-login carries the
29163
+ * new user's proven identity and therefore still transfers.
29164
+ * - First CLAIM (tab has no owner yet — bootstrap): honored, so a
29165
+ * header-less / single-trust-domain deployment can still register
29166
+ * tabs. A *present* proven identity must still match the claimed
29167
+ * `userId` (you cannot bootstrap a tab under an identity you
29168
+ * cannot back).
29169
+ *
29170
+ * Returns whether ownership was (re)assigned so the HTTP layer can
29171
+ * surface the refusal without bricking the heartbeat loop.
29172
+ */
29173
+ recordRegistration(tabId, metadata, options) {
29138
29174
  const now = Date.now();
29175
+ const proven = options?.callerUserId;
29176
+ if (proven != null && proven !== metadata.userId) {
29177
+ return { ownershipAssigned: false, rejectedReason: "OWNER_CHANGE_UNVERIFIED" };
29178
+ }
29139
29179
  const existing = this.tabOwnership.get(tabId);
29140
29180
  if (existing && existing.userId === metadata.userId) {
29141
29181
  existing.sessionId = metadata.sessionId;
29142
29182
  existing.lastSeen = now;
29143
- return;
29183
+ return { ownershipAssigned: true };
29184
+ }
29185
+ if (existing) {
29186
+ if (proven == null) {
29187
+ return { ownershipAssigned: false, rejectedReason: "OWNER_CHANGE_UNVERIFIED" };
29188
+ }
29144
29189
  }
29145
29190
  this.tabOwnership.set(tabId, {
29146
29191
  userId: metadata.userId,
@@ -29148,6 +29193,7 @@ var CommandRelay = class {
29148
29193
  firstSeen: existing?.firstSeen ?? now,
29149
29194
  lastSeen: now
29150
29195
  });
29196
+ return { ownershipAssigned: true };
29151
29197
  }
29152
29198
  /**
29153
29199
  * Return the stored ownership record for a tab, or `null` if the tab