@robota-sdk/agent-transport-webrtc 3.0.0-beta.81

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.
Files changed (47) hide show
  1. package/LICENSE +661 -0
  2. package/dist/node/index.cjs +1 -0
  3. package/dist/node/index.d.cts +361 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +361 -0
  6. package/dist/node/index.d.ts.map +1 -0
  7. package/dist/node/index.js +2 -0
  8. package/dist/node/index.js.map +1 -0
  9. package/package.json +88 -0
  10. package/src/__tests__/admission-secure-by-default.test.ts +61 -0
  11. package/src/__tests__/admission-steps.test.ts +46 -0
  12. package/src/__tests__/cve-2024-29415-reachability.test.ts +80 -0
  13. package/src/__tests__/dtls-fingerprint-binding.test.ts +60 -0
  14. package/src/__tests__/handoff-grant-gate.test.ts +401 -0
  15. package/src/__tests__/local-peer-proof-gate.test.ts +213 -0
  16. package/src/__tests__/mesh-admission-contract.test.ts +38 -0
  17. package/src/__tests__/negotiated-certificate.test.ts +80 -0
  18. package/src/__tests__/operator-approval-gate.test.ts +306 -0
  19. package/src/__tests__/outbound-delivery-carrier.test.ts +152 -0
  20. package/src/__tests__/pairing-e2e.test.ts +238 -0
  21. package/src/__tests__/pairing-gate-e3.test.ts +192 -0
  22. package/src/__tests__/pairing-gate-e4.test.ts +108 -0
  23. package/src/__tests__/pairing-gate.test.ts +276 -0
  24. package/src/__tests__/pairing-relay-in-the-middle.test.ts +229 -0
  25. package/src/__tests__/session-attachment-budget.test.ts +136 -0
  26. package/src/__tests__/webrtc-transport.test.ts +349 -0
  27. package/src/__tests__/werift-loader.test.ts +18 -0
  28. package/src/__tests__/ws-signaling-client.test.ts +108 -0
  29. package/src/admission-steps.ts +55 -0
  30. package/src/channel-delivery.ts +44 -0
  31. package/src/handoff-grant-gate.ts +186 -0
  32. package/src/index.ts +21 -0
  33. package/src/local-peer-proof.ts +135 -0
  34. package/src/negotiated-certificate.ts +80 -0
  35. package/src/pairing-channel-lifecycle.ts +31 -0
  36. package/src/pairing-controllers.ts +79 -0
  37. package/src/pairing-frames.ts +34 -0
  38. package/src/pairing-gate-options.ts +129 -0
  39. package/src/pairing-gate.ts +376 -0
  40. package/src/session-attachment.ts +88 -0
  41. package/src/signaling.ts +62 -0
  42. package/src/transport-lifecycle-error.ts +11 -0
  43. package/src/webrtc-delivery-lifecycle.ts +51 -0
  44. package/src/webrtc-transport-options.ts +84 -0
  45. package/src/webrtc-transport.ts +328 -0
  46. package/src/werift-loader.ts +40 -0
  47. package/src/ws-signaling-client.ts +139 -0
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("@robota-sdk/agent-transport"),l=require("@robota-sdk/agent-transport/node"),u=require("@robota-sdk/agent-remote-pairing"),d=require("node:crypto"),f=require("node:module"),p=require("ws");p=s(p,1);function m(e,t){return(0,c.createOutboundDelivery)(t=>e.send(JSON.stringify(t)),(e,n)=>t(e,n),()=>e.bufferedAmount)}const h={"sha-256":`sha256`,"sha-384":`sha384`,"sha-512":`sha512`};function g(e,t){let n=h[t.toLowerCase()];if(!n)throw Error(`unsupported DTLS fingerprint algorithm: ${t}`);return(0,d.createHash)(n).update(e).digest(`hex`).toUpperCase().match(/../g)?.join(`:`)??``}function _(e,t,n,r){let i=e.sctpTransport?.dtlsTransport;if(!i)return r(`no DTLS transport for the data channel`),()=>void 0;let a=!1,o=()=>{if(a)return;let e=i.dtls?.remoteCertificate;if(a=!0,!e){r(`the DTLS layer exposed no remote certificate`);return}try{n(g(e,t))}catch(e){r(e instanceof Error?e.message:String(e))}};if(i.state===`connected`)return o(),()=>void 0;if(i.state===`failed`||i.state===`closed`)return r(`the DTLS handshake ended ${i.state}`),()=>void 0;let s=i.onStateChange.subscribe(e=>{if(e===`connected`)o();else if(e===`failed`||e===`closed`){if(a)return;a=!0,r(`the DTLS handshake ended ${e}`)}else return;s.unSubscribe()});return()=>s.unSubscribe()}const v=e=>(0,f.createRequire)(require("url").pathToFileURL(__filename).href)(e);function y(e=v){try{return e(`werift`)}catch{throw Error(`WebRTC transport unavailable — install the optional peer dependency "werift" to use @robota-sdk/agent-transport-webrtc.`)}}function b(e,t){let n=t===`operator-approval`;return e.localPeer!==void 0&&t!==`local-proof`&&t!==`handoff-grant`&&!n?`local-proof`:e.handoffGrant!==void 0&&t!==`handoff-grant`&&!n?`handoff-grant`:e.connectionApproval!==void 0&&!n?`operator-approval`:null}function x(e){return typeof e==`object`&&!!e&&e.t===`handoff-grant`&&e.grant!==void 0}function S(e){return{admitted:!1,trust:`unproven`,reason:e}}async function C(e,t){if(t===void 0)return S(`no hand-off grant verifier is configured for this channel`);if(!x(e))return S(`expected a handoff-grant frame carrying the signed grant`);try{let n=await t.verify(e.grant);return n.admitted&&n.trust===`same-user-same-host`?S(`the grant verifier returned same-user-same-host. A cross-device grant proves the user, not the machine — see SEC-010 for what same-host requires.`):!n.admitted||t.consent===void 0||await t.consent(n)?n:S(`the person at this machine declined the transfer`)}catch(e){return S(`the grant verifier could not decide: ${e instanceof Error?e.message:`unknown error`}`)}}function w(e){return{t:`handoff-grant`,grant:e}}function T(e){return typeof e==`object`&&!!e&&e.t===`local-proof`&&typeof e.nonce==`string`}function E(e){return{admitted:!1,trust:`unproven`,reason:e}}function D(e,t){if(t===void 0)return E(`no local-peer proof is configured for this channel`);if(!T(e))return E(`expected a local-proof frame carrying the rendezvous nonce`);try{return t.redeem(e.nonce)}catch(e){return E(`the rendezvous ledger could not decide: ${e instanceof Error?e.message:`unknown error`}`)}}function O(e){return{t:`local-proof`,nonce:e}}function k(e,t){try{e.send(t)}catch{}}function A(e){try{e.close()}catch{}}function j(e,t,n){try{e?.(t,n)}catch{}}const M=Object.freeze({send:k,close:A,reportDeliveryError:j});function N(e,t,n,r,i,a=u.startPairingHandshake){let o=a({secret:t,role:n,localFingerprint:e.localFingerprint,remoteFingerprint:e.remoteFingerprint,send:t=>M.send(e.channel,JSON.stringify(t)),...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}});return o.result.then(r,i),o}function P(e,t,n,r){let i=(0,u.startHostReconnect)({hostIdentityId:t.hostIdentityId,localFingerprint:e.localFingerprint,remoteFingerprint:e.remoteFingerprint,hostPrivateKey:t.hostPrivateKey,resolveDevicePublicKey:t.resolveDevicePublicKey,send:t=>M.send(e.channel,JSON.stringify(t)),...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}});return i.result.then(n,r),i}function F(e){return(0,u.decodePairingFrame)(e).ok}function I(e){return(0,u.decodeReconnectFrame)(e).ok}function L(e){return(0,u.decodeEnrollFrame)(e).ok}function R(e,t,n){let r=e.resumeBridge;if(r)return r.attach(t=>e.channel.send(t),{awaitResume:t,onDeliveryError:n,pendingBytes:()=>e.channel.bufferedAmount}),{onSessionMessage:e=>r.onClientMessage(e),cleanup:()=>r.detach()};let{onMessage:i,cleanup:a}=(e.createHandler??c.createSessionMessageHandler)({session:e.session,deliver:m(e.channel,n),...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{},...e.surface?{surface:e.surface}:{}});return{onSessionMessage:i,cleanup:a}}var z=class{options;state;onSessionMessage;handlerCleanup;pairingController;reconnectController;pendingResult;pendingViaReconnect=!1;pendingDeviceId;pendingEnrollment;heldFrames=[];heldChars=0;approvalAbort;constructor(e){this.options=e,e.reconnect?this.state=`awaiting-mode`:(this.state=`pairing`,this.startFirstPair())}onInbound(e){if(this.state===`closed`)return;if(this.state===`accepted`){this.onSessionMessage?.(e);return}if(this.state===`operator-approval`){this.holdForApproval(e);return}let t;try{t=JSON.parse(e)}catch{return}if(this.state===`awaiting-mode`){I(t)&&t.t===`rc-hello`?(this.state=`reconnecting`,this.startReconnect(),this.reconnectController?.onFrame(t)):F(t)&&t.t===`pair-nonce`&&(this.state=`pairing`,this.startFirstPair(),this.pairingController?.onFrame(t));return}if(this.state===`pairing`){F(t)&&this.pairingController?.onFrame(t);return}if(this.state===`reconnecting`){I(t)&&this.reconnectController?.onFrame(t);return}if(this.state===`local-proof`){let e=D(t,this.options.localPeer);this.options.localPeer?.onAdmission?.(e),e.admitted?this.accept(this.pendingResult,this.pendingViaReconnect):this.rejectAndClose();return}if(this.state===`handoff-grant`){this.judgeGrantFrame(t);return}if(this.state===`enrolling`){L(t)&&this.completeEnrollment(t.spki);return}}cleanup(){this.state=`closed`,this.withdrawApproval(),this.handlerCleanup?.(),this.handlerCleanup=void 0,this.onSessionMessage=void 0}onChannelClosed(){this.state!==`accepted`&&this.state!==`closed`&&this.rejectAndClose()}withdrawApproval(){this.heldFrames=[],this.heldChars=0,this.approvalAbort?.abort(),this.approvalAbort=void 0}startFirstPair(){this.pairingController=N(this.options,this.options.secret,this.options.role,e=>this.onFirstPairAccepted(e),()=>this.rejectAndClose(),this.options.startHandshake??u.startPairingHandshake)}startReconnect(){let e=this.options.reconnect;if(!e){this.rejectAndClose();return}this.reconnectController=P(this.options,e,e=>{this.pendingDeviceId=e.deviceId,this.accept(void 0,!0)},()=>this.rejectAndClose())}onFirstPairAccepted(e){if(this.state!==`pairing`)return;this.pendingResult=e;let t=this.options.reconnect;if(!t){this.accept(e);return}this.state=`enrolling`,M.send(this.options.channel,JSON.stringify({t:`enroll-key`,spki:t.hostPublicSpki}))}completeEnrollment(e){if(this.state===`enrolling`){if(!this.options.reconnect){this.rejectAndClose();return}(async()=>{try{await(0,u.importPublicKey)(e);let t=await(0,u.deriveIdentityId)(e);if(this.state!==`enrolling`)return;this.pendingDeviceId=t,this.pendingEnrollment={deviceId:t,spki:e},this.accept(this.pendingResult)}catch{this.rejectAndClose()}})()}}accept(e,t=!1){if(this.state===`closed`||this.state===`accepted`)return;let n=b(this.options,this.state);if(n!==null){this.pendingResult=e??this.pendingResult,this.pendingViaReconnect=t,this.state=n,n===`operator-approval`&&this.askOperator();return}let r=this.pendingEnrollment;if(this.pendingEnrollment=void 0,r!==void 0)try{this.options.reconnect?.onEnroll(r.deviceId,r.spki)}catch{this.rejectAndClose();return}let i=R(this.options,t,(e,t)=>this.handleSessionDeliveryError(e,t));this.onSessionMessage=i.onSessionMessage,this.handlerCleanup=i.cleanup,this.state=`accepted`,this.options.onAccept?.(e);let a=this.heldFrames;this.heldFrames=[],this.heldChars=0;for(let e of a){if(this.state!==`accepted`)return;this.onSessionMessage?.(e)}}holdForApproval(e){this.heldChars+=e.length,this.heldFrames.push(e),(this.heldFrames.length>32||this.heldChars>262144)&&this.rejectAndClose()}async askOperator(){let e=new AbortController;this.approvalAbort=e;let t={...this.pendingDeviceId===void 0?{}:{deviceId:this.pendingDeviceId},viaReconnect:this.pendingViaReconnect,signal:e.signal},n=!1;try{n=await this.options.connectionApproval?.approve(t)===!0}catch{n=!1}this.state!==`operator-approval`||e.signal.aborted||(this.approvalAbort=void 0,n?this.accept(this.pendingResult,this.pendingViaReconnect):this.rejectAndClose())}async judgeGrantFrame(e){let t=await C(e,this.options.handoffGrant);this.state===`handoff-grant`&&(this.options.handoffGrant?.onAdmission?.(t),t.admitted?this.accept(this.pendingResult,this.pendingViaReconnect):this.rejectAndClose())}rejectAndClose(){this.state!==`closed`&&(this.state=`closed`,this.withdrawApproval(),M.close(this.options.channel),this.options.onReject?.())}handleSessionDeliveryError(e,t){this.state!==`closed`&&(this.state=`closed`,this.handlerCleanup?.(),this.handlerCleanup=void 0,this.onSessionMessage=void 0,M.reportDeliveryError(this.options.onDeliveryError,e,t),M.close(this.options.channel))}};function B(e){return Object.assign(Error(`WebRtcTransport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`webrtc`})}var V=class{options;generation=0;paired=!1;dropped=!1;constructor(e){this.options=e}reset(e){this.generation=e,this.paired=!1,this.dropped=!1}accept(e){e===this.generation&&(this.paired=!0)}handleDrop(e){e!==this.generation||!this.paired||this.dropped||(this.dropped=!0,this.options.cleanup(),this.options.onDropped())}handleFailure(e,t,n,r){if(t!==this.generation||this.dropped)return;let i=this.paired;i&&(this.dropped=!0);try{this.options.onDeliveryError(n,r)}catch{}this.options.cleanup();try{e.close()}catch{}i&&this.options.onDropped()}},H=class{options;name=`webrtc`;lifecycle=Object.freeze({kind:`service`});defaultEnabled=!1;optionsSchema={};session;peer;unsubscribeSignal;cleanupHandler;generation=0;localFingerprint;pairingGate;answered=!1;pendingFrames=[];stopAwaitingCertificate;deliveryLifecycle;constructor(e){if(this.options=e,this.deliveryLifecycle=new V({cleanup:()=>{this.cleanupHandler?.(),this.pairingGate?.cleanup()},onDropped:()=>this.options.onDropped?.(),onDeliveryError:(e,t)=>this.options.onDeliveryError?.(e,t)}),this.options.secret&&this.options.open===!0)throw Error("WebRtcTransport: `secret` and `open: true` are contradictory. A pairing secret gates the data channel; `open` runs without a gate. Pass one.");if(this.options.secret===void 0||this.options.secret===``){if(this.options.open!==!0)throw Error('WebRtcTransport: no pairing `secret` and no explicit `open`. Pass a `secret` to gate the data channel, or `{ open: true, openReason: "…" }` to run without pairing on purpose.');(0,l.resolveAdmission)({open:!0,...this.options.openReason===void 0?{}:{openReason:this.options.openReason}})}}validateOptions(){return!0}attach(e){this.session=e}createPeer(){let{RTCPeerConnection:e}=(this.options.loadWerift??y)(),t={};return this.options.iceServers&&(t.iceServers=this.options.iceServers.map(e=>({...e}))),this.options.forceTurn&&(t.iceTransportPolicy=`relay`),new e(Object.keys(t).length>0?t:void 0)}wireSignaling(e,t,n,r){let i=this.options.signaling;e.onIceCandidate.subscribe(t=>{t&&r===this.generation&&e===this.peer&&i.send({kind:`ice`,data:t.toJSON()})});let a=Promise.resolve();this.unsubscribeSignal=i.onSignal(i=>{r===this.generation&&(a=a.then(async()=>{if(r===this.generation&&e===this.peer)if(i.kind===`answer`){if(this.answered)return;this.answered=!0;let a=this.remoteFingerprintAlgorithm(t,i.data);if(this.options.secret&&a===void 0)return;await e.setRemoteDescription(i.data),a!==void 0&&this.awaitVerifiedCertificate(e,t,n,a,r)}else i.kind===`ice`&&await e.addIceCandidate(i.data)}))})}async requireCurrentPeer(e,t){if(t!==this.generation||this.peer!==e)throw await e.close(),Error(`WebRtcTransport startup was stopped.`)}async start(){let e=this.session;if(!e)throw B(`not-attached`);if(this.peer)throw B(`already-started`);let t=++this.generation;this.deliveryLifecycle.reset(t),this.answered=!1,this.pendingFrames=[];let n=this.createPeer();this.peer=n;let r=this.options.signaling,i=n.createDataChannel(`robota-session`);this.wireChannel(i,e,t),this.wireSignaling(n,i,e,t);let a=await n.createOffer();await this.requireCurrentPeer(n,t),await n.setLocalDescription(a),await this.requireCurrentPeer(n,t),this.options.secret&&n.localDescription&&(this.localFingerprint=(0,u.extractDtlsFingerprint)(n.localDescription.sdp)),r.send({kind:`offer`,data:n.localDescription})}remoteFingerprintAlgorithm(e,t){if(!this.options.secret)return;let n=t.sdp;try{if(typeof n!=`string`)throw Error(`answer carries no SDP`);return(0,u.extractDtlsFingerprintAttribute)(n).algorithm}catch{this.failPairing(e);return}}failPairing(e){try{e.close()}catch{}this.options.onPairingFailed?.()}awaitVerifiedCertificate(e,t,n,r,i){this.stopAwaitingCertificate=_(e,r,e=>{i===this.generation&&this.startPairing(t,n,e,i)},()=>{i===this.generation&&this.failPairing(t)})}startPairing(e,t,n,r){let i=this.options.secret;if(!i||!this.localFingerprint)return;this.pairingGate=new z({channel:{send:t=>e.send(t),close:()=>void e.close()},session:t,secret:i,role:`initiator`,localFingerprint:this.localFingerprint,remoteFingerprint:n,onAccept:e=>{r===this.generation&&(this.deliveryLifecycle.accept(r),this.options.onPaired?.(e))},...this.options.onPairingFailed?{onReject:this.options.onPairingFailed}:{},...this.options.reconnect?{reconnect:this.options.reconnect}:{},...this.options.localPeer?{localPeer:this.options.localPeer}:{},...this.options.connectionApproval?{connectionApproval:this.options.connectionApproval}:{},...this.options.resumeBridge?{resumeBridge:this.options.resumeBridge}:{},...this.options.personalUsageReporter?{personalUsageReporter:this.options.personalUsageReporter}:{},...this.options.usageReporter?{usageReporter:this.options.usageReporter}:{},...this.options.storedSessionUsageReporter?{storedSessionUsageReporter:this.options.storedSessionUsageReporter}:{},surface:`remote`,onDeliveryError:(t,n)=>this.deliveryLifecycle.handleFailure(e,r,t,n)});let a=this.pairingGate;for(let e of this.pendingFrames.splice(0))a.onInbound(e)}wireChannel(e,t,n){if(this.options.secret){e.onMessage.subscribe(e=>{if(n!==this.generation)return;let t=typeof e==`string`?e:e.toString();this.pairingGate?this.pairingGate.onInbound(t):this.pendingFrames.length<16&&this.pendingFrames.push(t)}),e.stateChanged.subscribe(e=>{n===this.generation&&(e===`closed`||e===`closing`)&&(this.pairingGate?.onChannelClosed(),this.deliveryLifecycle.handleDrop(n))}),this.cleanupHandler=()=>this.pairingGate?.cleanup();return}let{onMessage:r,cleanup:i}=(0,c.createSessionMessageHandler)({session:t,deliver:m(e,(t,r)=>this.deliveryLifecycle.handleFailure(e,n,t,r)),...this.options.personalUsageReporter?{personalUsageReporter:this.options.personalUsageReporter}:{},...this.options.usageReporter?{usageReporter:this.options.usageReporter}:{},...this.options.storedSessionUsageReporter?{storedSessionUsageReporter:this.options.storedSessionUsageReporter}:{},surface:`remote`});this.cleanupHandler=i,e.onMessage.subscribe(e=>{n===this.generation&&r(typeof e==`string`?e:e.toString())})}async stop(){this.generation+=1,this.cleanupHandler?.(),this.unsubscribeSignal?.(),this.cleanupHandler=void 0,this.unsubscribeSignal=void 0,this.pairingGate=void 0,this.localFingerprint=void 0,this.answered=!1,this.pendingFrames=[],this.stopAwaitingCertificate?.(),this.stopAwaitingCertificate=void 0,this.deliveryLifecycle.reset(this.generation),this.peer&&=(await this.peer.close(),void 0),this.session=void 0}};function U(){let e=[],t=[],n=!0;function r(e,t){return{send(e){n&&queueMicrotask(()=>{for(let n of t)n(e)})},onSignal(t){return e.push(t),()=>{let n=e.indexOf(t);n>=0&&e.splice(n,1)}},close(){n=!1,e.length=0}}}return[r(e,t),r(t,e)]}const W=new Set([`offer`,`answer`,`ice`]);function G(e){return new p.default(e)}var K=class{options;socket;handlers=[];outbox=[];joined=!1;closed=!1;constructor(e){this.options=e;let t=e.createSocket??G;this.socket=t(e.url),this.socket.on(`open`,()=>this.handleOpen()),this.socket.on(`message`,e=>this.handleMessage(e)),this.socket.on(`error`,e=>this.fail(e instanceof Error?e:Error(String(e)))),this.socket.on(`close`,()=>this.handleClose())}handleOpen(){this.socket.send(JSON.stringify({type:`join`,rendezvous:this.options.rendezvous}));for(let e of this.outbox)this.writeSignal(e);this.outbox.length=0}handleMessage(e){let t;try{t=JSON.parse(typeof e==`string`?e:String(e))}catch{return}if(typeof t!=`object`||!t)return;let n=t;if(n.type===`joined`){this.joined=!0,this.options.onReady?.();return}if(n.type===`error`){this.fail(Error(`signaling relay rejected the connection: ${String(n.reason)}`));return}if(n.type===`signal`&&typeof n.kind==`string`&&W.has(n.kind)){let e={kind:n.kind,data:n.data};for(let t of this.handlers)t(e)}}handleClose(){this.closed||this.joined||this.fail(Error(`signaling socket closed before the rendezvous was joined`))}fail(e){this.options.onError?.(e)}writeSignal(e){this.socket.send(JSON.stringify({type:`signal`,kind:e.kind,data:e.data}))}send(e){this.closed||(this.socket.readyState===1?this.writeSignal(e):this.outbox.push(e))}onSignal(e){return this.handlers.push(e),()=>{let t=this.handlers.indexOf(e);t>=0&&this.handlers.splice(t,1)}}close(){this.closed=!0,this.handlers.length=0,this.outbox.length=0,this.socket.close()}};exports.WebRtcTransport=H,exports.WsSignalingClient=K,exports.createInMemorySignalingPair=U,exports.handoffGrantFrame=w,exports.loadWerift=y,exports.localProofFrame=O;
@@ -0,0 +1,361 @@
1
+ import { IConfigurableTransport } from "@robota-sdk/agent-interface-transport";
2
+ import { IProtocolSession, ISessionMessageHandlerOptions, SessionResumeBridge } from "@robota-sdk/agent-transport";
3
+ import { IPairingResult } from "@robota-sdk/agent-remote-pairing";
4
+ import { IPeerAdmission } from "@robota-sdk/agent-interface-session-mobility";
5
+ import { RTCPeerConnection } from "werift";
6
+ //#region src/handoff-grant-gate.d.ts
7
+ /** The frame a source device presents to show it holds a grant for this transfer. */
8
+ interface IHandoffGrantFrame {
9
+ readonly t: 'handoff-grant';
10
+ /**
11
+ * The signed grant, carried opaquely.
12
+ *
13
+ * `unknown` on purpose: this package cannot import the grant type without importing the package
14
+ * that owns the crypto, and typing it structurally here would create a second declaration of a
15
+ * signed object's shape — which is how a field ends up checked in one copy and not the other.
16
+ * The verifier owns the type; this gate owns the envelope.
17
+ */
18
+ readonly grant: unknown;
19
+ }
20
+ /**
21
+ * The cross-device authorization this gate requires before exposing the session.
22
+ *
23
+ * Absent → the gate behaves exactly as before. Requiring a grant is opt-in because an ordinary
24
+ * remote peer has no hand-off to authorize, and a gate that demanded one unconditionally would
25
+ * refuse every legitimate remote session.
26
+ */
27
+ interface IHandoffGrantProof {
28
+ /**
29
+ * Judge a presented grant. Owned by the verifier — this package asks and does not decide.
30
+ *
31
+ * Async because verifying a signature is: `verifyHandoffGrant` returns a promise, and a
32
+ * synchronous seam here would force the owner to block or to pre-verify, and pre-verifying means
33
+ * deciding before the channel that the grant is bound to even exists.
34
+ *
35
+ * Returns the admission the consumer receives, so the trust level travels as a value.
36
+ */
37
+ readonly verify: (grant: unknown) => Promise<IPeerAdmission>;
38
+ /**
39
+ * Ask the person at THIS machine whether to accept the transfer. Absent → no consent is required.
40
+ *
41
+ * Called ONLY on a verdict the verifier admitted, and given that verdict, for two reasons that
42
+ * both matter:
43
+ *
44
+ * The prompt can then name a PROVEN origin. Asking before verification would let anyone who can
45
+ * open a channel raise a dialog on someone's machine claiming to be any device they like, which
46
+ * turns the consent step into an attack surface instead of a control.
47
+ *
48
+ * And consent is not cryptographic policy, so it does not belong inside `verify`. A verifier that
49
+ * also asked a human would make "is this grant valid" and "does this person want it" one answer,
50
+ * and the first is the one that must be decidable without a person present.
51
+ *
52
+ * Returning false — or throwing, or having no renderer to ask — refuses. Denial fails CLOSED.
53
+ */
54
+ readonly consent?: (admission: IPeerAdmission) => Promise<boolean>;
55
+ /**
56
+ * Fired on the outcome, admitted or not.
57
+ *
58
+ * On BOTH, deliberately: a consumer told only about successes cannot distinguish "no hand-off was
59
+ * offered" from "a hand-off was refused", and those call for different operator responses — the
60
+ * second is the one that belongs in front of a person.
61
+ */
62
+ readonly onAdmission?: (admission: IPeerAdmission) => void;
63
+ }
64
+ /**
65
+ * Build the frame a source device presents on the channel.
66
+ *
67
+ * Lives beside the judge that reads it, so the two cannot drift: a sender that hand-built the object
68
+ * would keep compiling after the frame gains a field, and the failure would surface as a refusal on
69
+ * the far side with no hint that the SENDER is the stale half.
70
+ *
71
+ * Deliberately not a "send" — it returns the frame and leaves transmission to whoever owns the
72
+ * channel, for the same reason `localProofFrame` does.
73
+ */
74
+ declare function handoffGrantFrame(grant: unknown): IHandoffGrantFrame;
75
+ //#endregion
76
+ //#region src/local-peer-proof.d.ts
77
+ /** The frame a local peer presents to show it reached the guarded rendezvous. */
78
+ interface ILocalProofFrame {
79
+ readonly t: 'local-proof';
80
+ readonly nonce: string;
81
+ }
82
+ /**
83
+ * The local-environment proof this gate requires before exposing the session.
84
+ *
85
+ * Absent → the gate behaves exactly as before. Requiring the proof is opt-in because a remote peer
86
+ * over WebRTC has no rendezvous to have reached, and a gate that demanded one unconditionally would
87
+ * refuse every legitimate remote session.
88
+ */
89
+ interface ILocalPeerProof {
90
+ /**
91
+ * Redeem a presented nonce. Owned by the ledger — this package asks and does not decide.
92
+ *
93
+ * Returns the admission the consumer receives, so the trust level travels as a value rather than
94
+ * being re-derived from a boolean by whoever reads it next.
95
+ */
96
+ readonly redeem: (nonce: string) => IPeerAdmission;
97
+ /**
98
+ * Fired on the outcome, admitted or not.
99
+ *
100
+ * On BOTH, deliberately: a consumer told only about successes cannot distinguish "no local peer
101
+ * connected" from "a local peer was refused", and those call for different operator responses.
102
+ */
103
+ readonly onAdmission?: (admission: IPeerAdmission) => void;
104
+ }
105
+ /**
106
+ * Build the frame a local peer presents on the channel.
107
+ *
108
+ * Lives beside the judge that reads it, so the two cannot drift: a sender that hand-built the object
109
+ * would keep compiling after the frame gains a field, and the failure would surface as a refusal on
110
+ * the far side with no hint that the SENDER is the stale half.
111
+ *
112
+ * Deliberately not a "send" — it returns the frame and leaves transmission to whoever owns the
113
+ * channel. A helper that both built and sent would need a channel to be testable, and would put the
114
+ * frame's shape and the carrier's lifecycle in one place.
115
+ */
116
+ declare function localProofFrame(nonce: string): ILocalProofFrame;
117
+ //#endregion
118
+ //#region src/pairing-gate-options.d.ts
119
+ /** E3 host reconnect/enrollment config. When present, the gate runs reactive (first-frame) mode detection. */
120
+ interface IHostReconnectConfig {
121
+ readonly hostIdentityId: string;
122
+ /** base64url SPKI advertised to a device at first-pair enrollment. */
123
+ readonly hostPublicSpki: string;
124
+ /** The host identity private key (signs reconnect challenges). */
125
+ readonly hostPrivateKey: CryptoKey;
126
+ /** Resolve a pinned device public key by id (undefined → unknown/revoked → fail closed). */
127
+ readonly resolveDevicePublicKey: (deviceId: string) => Promise<CryptoKey | undefined>;
128
+ /** Pin a device's public key on first-pair enrollment (deviceId, base64url SPKI). */
129
+ readonly onEnroll: (deviceId: string, deviceSpki: string) => void;
130
+ }
131
+ /** What the receiving operator is told about a connection that would drive the session. */
132
+ interface IConnectionApprovalContext {
133
+ /** The device the handshake proved, when the carrier has a stable id for it. */
134
+ readonly deviceId?: string;
135
+ /** A trusted device coming back, rather than one pairing now. */
136
+ readonly viaReconnect: boolean;
137
+ /** Aborted when the connection goes away before an answer: withdraw the question. */
138
+ readonly signal: AbortSignal;
139
+ }
140
+ /**
141
+ * The receiving operator's say over one connection. `approve` resolves `true` only on the operator's
142
+ * explicit yes; `false` or a rejection refuses the connection.
143
+ */
144
+ interface IConnectionApproval {
145
+ approve(context: IConnectionApprovalContext): Promise<boolean>;
146
+ }
147
+ //#endregion
148
+ //#region src/signaling.d.ts
149
+ /**
150
+ * Signaling port (REMOTE-002). The WebRTC transport is decoupled from any concrete signaling server via this
151
+ * port: it exchanges opaque SDP/ICE blobs by rendezvous id. The port carries **only** SDP offers/answers + ICE
152
+ * candidates — never session content — so a signaling server (or the in-memory test pair) is content-blind.
153
+ */
154
+ /** The kinds of signal a peer exchanges through the relay. */
155
+ type TSignalKind = 'offer' | 'answer' | 'ice';
156
+ /** An opaque signaling message relayed by rendezvous id. `data` is an SDP description or an ICE candidate. */
157
+ interface ISignalMessage {
158
+ readonly kind: TSignalKind;
159
+ /** Opaque payload (serialized SDP or ICE candidate). The signaling layer never inspects it. */
160
+ readonly data: unknown;
161
+ }
162
+ /** Port a WebRTC peer uses to reach its counterpart at a rendezvous id. */
163
+ interface ISignalingClient {
164
+ /** Send a signal to the counterpart at the rendezvous. */
165
+ send(message: ISignalMessage): void;
166
+ /** Subscribe to signals from the counterpart. Returns an unsubscribe function. */
167
+ onSignal(handler: (message: ISignalMessage) => void): () => void;
168
+ /** Tear down the signaling channel. */
169
+ close(): void;
170
+ }
171
+ /**
172
+ * An in-process pair of {@link ISignalingClient}s wired directly to each other — for tests + loopback only
173
+ * (no server, no network). Each side's `send` is delivered to the other side's handlers on a microtask.
174
+ */
175
+ declare function createInMemorySignalingPair(): [ISignalingClient, ISignalingClient];
176
+ //#endregion
177
+ //#region src/werift-loader.d.ts
178
+ /** The subset of the `werift` module surface this transport constructs. */
179
+ interface IWeriftModule {
180
+ RTCPeerConnection: new (configuration?: {
181
+ iceServers?: {
182
+ urls: string;
183
+ username?: string;
184
+ credential?: string;
185
+ }[];
186
+ iceTransportPolicy?: 'all' | 'relay';
187
+ }) => RTCPeerConnection;
188
+ }
189
+ /**
190
+ * Lazily load the optional `werift` (pure-TypeScript WebRTC) peer dependency (REMOTE-002). `werift` is kept
191
+ * OUT of the transport's runtime dependency graph (peer + optional) so this package is importable without it;
192
+ * absence surfaces an **explicit "WebRTC transport unavailable" error at point-of-use** (a throw, mirroring
193
+ * `agent-cli`'s `loadReplayProvider`) — never a silent no-op or degraded path (no-fallback rule). The
194
+ * werift-vs-native choice is a recorded design decision, not a runtime fallback.
195
+ */
196
+ /** Resolve a module by id. The default resolves the real `werift`; tests inject a throwing resolver. */
197
+ type TModuleResolver = (id: string) => unknown;
198
+ declare function loadWerift(resolve?: TModuleResolver): IWeriftModule;
199
+ //#endregion
200
+ //#region src/webrtc-transport-options.d.ts
201
+ /**
202
+ * A single ICE (STUN/TURN) server for the HOST (werift) transport (REMOTE-010). `urls` is a SINGLE string with a
203
+ * `turn:`/`turns:`/`stun:`/`stuns:` scheme — werift's ICE gatherer (`parseIceServers`) consumes only a single-string
204
+ * url and silently drops array `urls`, so the host reader (`agent-cli` `parseIceServers`) must narrow to this shape
205
+ * and reject what werift would drop (fail-closed). (The browser peer uses the native DOM `RTCIceServer`, which does
206
+ * support array urls / `turns:` — a separate, wider validator.) Kept a plain interface (no DOM dependency here).
207
+ */
208
+ interface IIceServer {
209
+ readonly urls: string;
210
+ readonly username?: string;
211
+ readonly credential?: string;
212
+ }
213
+ /** Construction options for {@link WebRtcTransport}. The signaling client is injected (Stage A: no settings). */
214
+ interface IWebRtcTransportOptions {
215
+ /** Host-owned usage read models, available only after admission. */
216
+ readonly personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
217
+ readonly usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
218
+ readonly storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
219
+ /** Signaling port used to exchange SDP/ICE with the remote peer by rendezvous id. */
220
+ readonly signaling: ISignalingClient;
221
+ /** Optional ICE servers (STUN/TURN). Omitted → host-candidate/loopback only. */
222
+ readonly iceServers?: readonly IIceServer[];
223
+ /**
224
+ * REMOTE-004 defense-in-depth: when true, restrict ICE to **relay (TURN) candidates only**, so
225
+ * host/server-reflexive candidates — and the local-interface gathering that touches the (unreachable, but
226
+ * belt-and-braces) `ip` code path — are never used. Requires a TURN server in `iceServers`. Mapped to werift's
227
+ * `iceTransportPolicy: 'relay'` (REMOTE-010) — werift IGNORES a top-level `forceTurn`, so it must NOT be passed.
228
+ */
229
+ readonly forceTurn?: boolean;
230
+ /**
231
+ * REMOTE-008 pairing secret. When set, the data channel is **pairing-gated**: it carries only pairing frames
232
+ * until the directional-HMAC handshake accepts (channel-bound to the DTLS fingerprints), and only THEN is the
233
+ * session exposed — fail closed on mismatch/timeout. When omitted (Stage-A loopback / tests), the channel is
234
+ * exposed immediately with no pairing (unchanged behavior).
235
+ */
236
+ readonly secret?: string;
237
+ /**
238
+ * SEC-008: run with NO pairing gate. Requires `openReason`.
239
+ *
240
+ * Omitting `secret` used to mean this implicitly, which is how a remote peer reached the session
241
+ * because a field was left unset. It is still a legitimate mode — loopback, tests — but it is now
242
+ * a thing the host says rather than a thing that happens.
243
+ */
244
+ readonly open?: boolean;
245
+ /** SEC-008: why running with no pairing gate is correct here. Required when `open` is true. */
246
+ readonly openReason?: string;
247
+ /** REMOTE-008: fired when pairing accepts + the session is exposed (host lifecycle → status 'paired'). Carries the first-pair result (E4 uses its sessionKey). */
248
+ readonly onPaired?: (result?: IPairingResult) => void;
249
+ /** REMOTE-008: fired when pairing rejects/times out (host lifecycle → teardown; the channel is already closed). */
250
+ readonly onPairingFailed?: () => void;
251
+ /** REMOTE-012 E3: host reconnect/enrollment config. When set, the gate admits first-pair (with enrollment) OR a pinned-device reconnect. */
252
+ readonly reconnect?: IHostReconnectConfig;
253
+ /** SEC-010 (#1810): require a guarded-rendezvous nonce before the session is exposed. Absent → unchanged. */
254
+ readonly localPeer?: ILocalPeerProof;
255
+ /** Ask the receiving operator before each connection reaches the session. Absent → unchanged. */
256
+ readonly connectionApproval?: IConnectionApproval;
257
+ /** REMOTE-013 E4: a session-scoped resume bridge (owned by the controller across reconnects). Passed to the gate so the paired session flows through it (seq/buffer) and survives channel drops. */
258
+ readonly resumeBridge?: SessionResumeBridge;
259
+ /** REMOTE-013 E4: fired when a PAIRED data channel drops (so the controller can run the reconnect loop). Not fired for a pre-accept failure (that is `onPairingFailed`). */
260
+ readonly onDropped?: () => void;
261
+ /** Observe an outbound session-event delivery failure before the carrier drops. */
262
+ readonly onDeliveryError?: (error: Error, event: string) => void;
263
+ /** Test seam: inject the werift module (defaults to the real lazy loader). */
264
+ readonly loadWerift?: () => IWeriftModule;
265
+ }
266
+ //#endregion
267
+ //#region src/webrtc-transport.d.ts
268
+ /**
269
+ * WebRTC P2P transport (REMOTE-001/002): carries an `IProtocolSession` over an `RTCDataChannel` using the
270
+ * SAME transport-neutral session bridge as the WebSocket transport (`createSessionMessageHandler` from
271
+ * `@robota-sdk/agent-transport`). The host is the offerer: it creates the data channel + offer, and on
272
+ * data-channel open wires the handler. **Stage A: `defaultEnabled: false`, no pairing/auth** — the signaling
273
+ * client is injected and can be an in-memory loopback for tests.
274
+ */
275
+ declare class WebRtcTransport implements IConfigurableTransport<IProtocolSession> {
276
+ private readonly options;
277
+ readonly name = "webrtc";
278
+ readonly lifecycle: Readonly<{
279
+ kind: "service";
280
+ }>;
281
+ readonly defaultEnabled = false;
282
+ readonly optionsSchema: {};
283
+ private session?;
284
+ private peer?;
285
+ private unsubscribeSignal?;
286
+ private cleanupHandler?;
287
+ /** Invalidates pending async startup work and scopes pairing/drop state to one start generation. */
288
+ private generation;
289
+ /** Local DTLS fingerprint captured for pairing channel binding. */
290
+ private localFingerprint?;
291
+ /** Pairing gate for the current channel. */
292
+ private pairingGate?;
293
+ /** Whether this start generation has taken its one answer; any later answer is ignored. */
294
+ private answered;
295
+ /** Pre-gate channel frames, replayed into the gate once it exists. Bounded. */
296
+ private pendingFrames;
297
+ private stopAwaitingCertificate?;
298
+ private readonly deliveryLifecycle;
299
+ constructor(options: IWebRtcTransportOptions);
300
+ validateOptions(): boolean;
301
+ attach(session: IProtocolSession): void;
302
+ private createPeer;
303
+ private wireSignaling;
304
+ private requireCurrentPeer;
305
+ start(): Promise<void>;
306
+ /**
307
+ * With a pairing secret, the answer must advertise exactly one DTLS fingerprint; its algorithm is the one the
308
+ * verified certificate is hashed with. A refused answer fails pairing and closes the channel.
309
+ */
310
+ private remoteFingerprintAlgorithm;
311
+ private failPairing;
312
+ /** Build the pairing gate once the DTLS layer has verified the remote certificate. */
313
+ private awaitVerifiedCertificate;
314
+ private startPairing;
315
+ private wireChannel;
316
+ stop(): Promise<void>;
317
+ }
318
+ //#endregion
319
+ //#region src/ws-signaling-client.d.ts
320
+ /** A minimal WebSocket-like surface — so tests can inject a fake without a real socket. */
321
+ interface IWebSocketLike {
322
+ send(data: string): void;
323
+ close(): void;
324
+ readyState: number;
325
+ on(event: 'open' | 'message' | 'error' | 'close', handler: (arg: unknown) => void): void;
326
+ }
327
+ interface IWsSignalingClientOptions {
328
+ /** Relay URL, e.g. `ws://127.0.0.1:1234`. */
329
+ readonly url: string;
330
+ /** Rendezvous id to join. */
331
+ readonly rendezvous: string;
332
+ /**
333
+ * Explicit error sink. Called on a relay `error` frame, a socket error, or a close-before-join — the signaling
334
+ * channel cannot function and the caller must know (no silent degrade).
335
+ */
336
+ readonly onError?: (error: Error) => void;
337
+ /** Called once the relay confirms the rendezvous `join` (the `joined` frame). */
338
+ readonly onReady?: () => void;
339
+ /** Injectable socket factory (defaults to a real `ws` WebSocket) — for tests. */
340
+ readonly createSocket?: (url: string) => IWebSocketLike;
341
+ }
342
+ declare class WsSignalingClient implements ISignalingClient {
343
+ private readonly options;
344
+ private readonly socket;
345
+ private readonly handlers;
346
+ private readonly outbox;
347
+ private joined;
348
+ private closed;
349
+ constructor(options: IWsSignalingClientOptions);
350
+ private handleOpen;
351
+ private handleMessage;
352
+ private handleClose;
353
+ private fail;
354
+ private writeSignal;
355
+ send(message: ISignalMessage): void;
356
+ onSignal(handler: (message: ISignalMessage) => void): () => void;
357
+ close(): void;
358
+ }
359
+ //#endregion
360
+ export { type IConnectionApproval, type IConnectionApprovalContext, type IHandoffGrantFrame, type IHandoffGrantProof, type IHostReconnectConfig, type IIceServer, type ILocalPeerProof, type ILocalProofFrame, type ISignalMessage, type ISignalingClient, type IWebRtcTransportOptions, type IWebSocketLike, type IWeriftModule, type IWsSignalingClientOptions, type TModuleResolver, type TSignalKind, WebRtcTransport, WsSignalingClient, createInMemorySignalingPair, handoffGrantFrame, loadWerift, localProofFrame };
361
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/handoff-grant-gate.ts","../../src/local-peer-proof.ts","../../src/pairing-gate-options.ts","../../src/signaling.ts","../../src/werift-loader.ts","../../src/webrtc-transport-options.ts","../../src/webrtc-transport.ts","../../src/ws-signaling-client.ts"],"mappings":";;;;;;;UA6CiB;WACN;;;;;;;;;WASA;;;;;;;;;UAUM;;;;;;;;;;WAUN,SAAS,mBAAmB,QAAQ;;;;;;;;;;;;;;;;;WAiBpC,WAAW,WAAW,mBAAmB;;;;;;;;WAQzC,eAAe,WAAW;;;;;;;;;;;;iBAmFrB,kBAAkB,iBAAiB;;;;UC9IlC;WACN;WACA;;;;;;;;;UAUM;;;;;;;WAON,SAAS,kBAAkB;;;;;;;WAO3B,eAAe,WAAW;;;;;;;;;;;;;iBAiErB,gBAAgB,gBAAgB;;;;UC1F/B;WACN;;WAEA;;WAEA,gBAAgB;;WAEhB,yBAAyB,qBAAqB,QAAQ;;WAEtD,WAAW,kBAAkB;;;UAIvB;;WAEN;;WAEA;;WAEA,QAAQ;;;;;;UAOF;EACf,QAAQ,SAAS,6BAA6B;;;;;;;;;;KC9DpC;;UAGK;WACN,MAAM;;WAEN;;;UAIM;;EAEf,KAAK,SAAS;;EAEd,SAAS,UAAU,SAAS;;EAE5B;;;;;;iBAOc,gCAAgC,kBAAkB;;;;UCzBjD;EACf,wBAAwB;IAItB;MAAe;MAAc;MAAmB;;IAChD;QACI;;;;;;;;;;KAWI,mBAAmB;iBAQf,WAAW,UAAS,kBAAoC;;;;;;;;;;UCNvD;WACN;WACA;WACA;;;UAIM;;WAEN,wBAAwB;WACxB,gBAAgB;WAChB,6BAA6B;;WAE7B,WAAW;;WAEX,sBAAsB;;;;;;;WAOtB;;;;;;;WAOA;;;;;;;;WAQA;;WAEA;;WAEA,YAAY,SAAS;;WAErB;;WAEA,YAAY;;WAEZ,YAAY;;WAEZ,qBAAqB;;WAErB,eAAe;;WAEf;;WAEA,mBAAmB,OAAO,OAAO;;WAEjC,mBAAmB;;;;;;;;;;;cCrDjB,2BAA2B,uBAAuB;mBAuBzB;WAtBpB;WACA,WAAS;;;WACT;WACA;UAER;UACA;UACA;UACA;;UAEA;;UAEA;;UAEA;;UAEA;;UAEA;UACA;mBACS;EAEmB,YAAA,SAAS;EA+BtC;EAIA,OAAO,SAAS;UAGf;UAYA;UAmCM;EAMD,SAAS;;;;;UA+Bd;UAYA;;UAUA;UAoBA;UA0CA;EAkDK,QAAQ;;;;;UCnSN;EACf,KAAK;EACL;EACA;EACA,GAAG,+CAA+C,UAAU;;UAG7C;;WAEN;;WAEA;;;;;WAKA,WAAW,OAAO;;WAElB;;WAEA,gBAAgB,gBAAgB;;cAU9B,6BAA6B;mBAOJ;mBANnB;mBACA;mBACA;UACT;UACA;EAE4B,YAAA,SAAS;UAWrC;UAOA;UA4BA;UAOA;UAIA;EAID,KAAK,SAAS;EASd,SAAS,UAAU,SAAS;EAQ5B"}