@robota-sdk/agent-transport-ws 3.0.0-beta.79 → 3.0.0-beta.82
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/README.md +14 -4
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +161 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +124 -175
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +36 -17
- package/src/__tests__/outbound-delivery-carrier.test.ts +195 -0
- package/src/__tests__/payload-channels.test.ts +233 -0
- package/src/__tests__/ws-payload-channel.e2e.test.ts +255 -0
- package/src/__tests__/ws-session-delivery.test.ts +86 -0
- package/src/__tests__/ws-transport-auth.test.ts +220 -0
- package/src/__tests__/ws-transport-lifecycle.test.ts +135 -0
- package/src/__tests__/ws-transport.test.ts +69 -5
- package/src/index.ts +5 -5
- package/src/payload-channels.ts +204 -0
- package/src/ws-connection-guards.ts +104 -0
- package/src/ws-message-data.ts +8 -0
- package/src/ws-session-delivery.ts +109 -0
- package/src/ws-transport-config.ts +114 -0
- package/src/ws-transport-configurable.ts +217 -40
- package/src/ws-transport.ts +60 -12
- package/src/__tests__/ws-handler.test.ts +0 -429
- package/src/ws-background-messages.ts +0 -170
- package/src/ws-handler.ts +0 -287
- package/src/ws-protocol.ts +0 -78
package/README.md
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
# @robota-sdk/agent-transport-ws
|
|
2
2
|
|
|
3
|
-
WebSocket
|
|
4
|
-
|
|
3
|
+
WebSocket carrier for the Robota SDK. The transport-neutral wire protocol, message types, and shared
|
|
4
|
+
session handler belong to `@robota-sdk/agent-transport`; this package adapts them to WebSocket
|
|
5
|
+
connections without pulling React, Ink, or Hono.
|
|
5
6
|
|
|
6
7
|
```typescript
|
|
7
|
-
import { WsTransport
|
|
8
|
-
import
|
|
8
|
+
import { WsTransport } from '@robota-sdk/agent-transport-ws';
|
|
9
|
+
import { createSessionMessageHandler } from '@robota-sdk/agent-transport';
|
|
10
|
+
import type { TServerMessage, TClientMessage } from '@robota-sdk/agent-transport';
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
See [docs/SPEC.md](./docs/SPEC.md) for the full contract.
|
|
14
|
+
|
|
15
|
+
The carrier forwards plan, context-refresh, and branch events using the shared serializable event types.
|
|
16
|
+
If a socket is not open or `send` fails asynchronously, the carrier runs one idempotent connection cleanup
|
|
17
|
+
path and reports the delivery failure without throwing back through the committed session operation.
|
|
18
|
+
|
|
19
|
+
Hosts may inject personal, stored-session, and current-session usage reporters when constructing the
|
|
20
|
+
transport. Each admitted connection receives those capabilities through the shared protocol handler;
|
|
21
|
+
the WebSocket transport itself never reads session stores or performs analytics.
|
package/dist/node/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("node:http"),
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@robota-sdk/agent-transport"),t=require("node:http"),n=require("ws"),r=require("node:crypto"),i=require("@robota-sdk/agent-transport/node");function a(t){let n=null,r=null,i=e=>Object.assign(Error(`WebSocket transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`ws`}),a={name:`ws`,lifecycle:Object.freeze({kind:`service`}),onMessage:null,attach(e){n=e},async start(){if(!n)throw i(`not-attached`);if(r)throw i(`already-started`);let o=(0,e.createOutboundDelivery)(t.send,(e,n)=>{s.cleanup(),r=null,a.onMessage=null,t.onDeliveryError?.(e,n)}),s=(0,e.createSessionMessageHandler)({session:n,deliver:o,...t.driverId?{driverId:t.driverId}:{},...t.surface?{surface:t.surface}:{},...t.personalUsageReporter?{personalUsageReporter:t.personalUsageReporter}:{},...t.usageReporter?{usageReporter:t.usageReporter}:{},...t.storedSessionUsageReporter?{storedSessionUsageReporter:t.storedSessionUsageReporter}:{}});r=s.cleanup,this.onMessage=s.onMessage},async stop(){r?.(),r=null,this.onMessage=null,n=null}};return a}var o=class t{channels=new Map;sinks=new Set;addSink(e){return this.sinks.add(e),()=>{this.sinks.delete(e)}}registerChannel(e){if(this.channels.has(e.name))throw Error(`Channel "${e.name}" is already registered on this transport`);let t={descriptor:e,declaredEvents:new Set(e.events),eventHandlers:new Map,binaryHandlers:new Set,seq:0,open:!0};return this.channels.set(e.name,t),this.createHandle(e,t)}receive(t){let n=(0,e.decodeChannelFrame)(t);if(!n.ok)return n;let{frame:r}=n,i=this.channels.get(r.channel);if(!i||!i.open)return{ok:!1,error:`No channel registered for "${r.channel}"`};if(r.kind===`binary`){if(i.descriptor.binary!==!0)return{ok:!1,error:`Channel "${r.channel}" did not declare binary payload support`};for(let e of[...i.binaryHandlers])e(r);return{ok:!0,frame:r}}if(!i.declaredEvents.has(r.event))return{ok:!1,error:`Channel "${r.channel}" did not declare event "${r.event}"`};let a=i.eventHandlers.get(r.event);if(a)for(let e of[...a])e(r.payload,r);return{ok:!0,frame:r}}broadcast(e){for(let t of[...this.sinks])t(e)}static assertOpen(e){if(!e.open)throw Error(`Channel "${e.descriptor.name}" is closed`)}createHandle(n,r){return{descriptor:n,sendEvent:(i,a)=>{if(t.assertOpen(r),!r.declaredEvents.has(i))throw Error(`Channel "${n.name}" did not declare event "${i}"`);let o=r.seq;r.seq+=1,this.broadcast((0,e.encodeChannelEventFrame)({kind:`event`,channel:n.name,seq:o,event:i,payload:a}))},sendBinary:i=>{if(t.assertOpen(r),n.binary!==!0)throw Error(`Channel "${n.name}" did not declare binary payload support`);let a=r.seq;r.seq+=1,this.broadcast((0,e.encodeBinaryFrame)({kind:`binary`,channel:n.name,seq:a,payload:i}))},onEvent:(e,i)=>{if(t.assertOpen(r),!r.declaredEvents.has(e))throw Error(`Channel "${n.name}" did not declare event "${e}"`);let a=r.eventHandlers.get(e)??new Set,o=i;return a.add(o),r.eventHandlers.set(e,a),()=>{a.delete(o)}},onBinary:e=>(t.assertOpen(r),r.binaryHandlers.add(e),()=>{r.binaryHandlers.delete(e)}),close:()=>{r.open=!1,r.eventHandlers.clear(),r.binaryHandlers.clear(),this.channels.delete(n.name)}}}};const s=new Set([`localhost`,`127.0.0.1`,`::1`,`[::1]`]);function c(e){if(!e)return null;if(e.startsWith(`[`))return e.slice(0,e.indexOf(`]`)+1)||null;let t=e.lastIndexOf(`:`);return t===-1?e:e.slice(0,t)}function l(e,t){let n=c(e.headers.host);return n===null?!1:s.has(n)||t.has(n)}function u(e,t){let n=e.headers.origin;if(n===void 0||t.has(n))return!0;try{return s.has(new URL(n).hostname)}catch{return!1}}function d(e,t){if(!t)return!1;let n=Buffer.from(e),i=Buffer.from(t);return n.length===i.length&&(0,r.timingSafeEqual)(n,i)}function f(e){try{let t=new URL(e.url??``,`ws://127.0.0.1`).searchParams.get(`token`);if(t)return t}catch{}let t=e.headers[`sec-websocket-protocol`];return typeof t==`string`&&t.split(`,`)[0]?.trim()||null}function p(e){return(0,i.resolveAdmission)({...e.token===void 0?{}:{token:e.token},...e.open===!0?{open:!0}:{},...e.openReason===void 0?{}:{openReason:e.openReason}})}function m(e){return Array.isArray(e)?new Uint8Array(Buffer.concat(e)):e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}var h=class{socket;cleanupProtocol=()=>void 0;detachSink=()=>void 0;closed=!1;stallClock=(0,e.createPendingStallClock)();deliver;constructor(t){this.socket=t,this.deliver=(0,e.createOutboundDelivery)(e=>this.rawSend(e),()=>this.close(),()=>this.socket.bufferedAmount,e.DEFAULT_MAX_PENDING_BYTES,this.stallClock)}deliverBinary=t=>{if(!this.closed&&this.socket.readyState===n.WebSocket.OPEN){if((0,e.isOverPendingBudget)(this.socket.bufferedAmount)||this.stallClock.observe(this.socket.bufferedAmount)!==void 0){this.close();return}this.socket.send(t,{binary:!0},e=>{e&&this.close()})}};rawSend(e){if(this.socket.readyState!==n.WebSocket.OPEN)throw Error(`WebSocket is not open`);this.socket.send(JSON.stringify(e),e=>{e&&this.close()})}bindProtocolCleanup(e){this.cleanupProtocol=e}bindSinkDetach(e){this.detachSink=e}close=()=>{this.closed||(this.closed=!0,this.detachSink(),this.cleanupProtocol(),(this.socket.readyState===n.WebSocket.OPEN||this.socket.readyState===n.WebSocket.CONNECTING)&&this.socket.close(1011,`session event delivery failed`))}};function g(e){return{...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{}}}function _(e){return{...e.driverId?{driverId:e.driverId}:{},...e.surface?{surface:e.surface}:{},...g(e)}}function v(e){let{port:t,maxRetries:n}=e;return t!==void 0&&(typeof t!=`number`||t<1||t>65535)?!1:n===void 0||typeof n==`number`&&n>=0}function y(e,t){return Object.assign(Error(`${e} ${t}.`),{name:`TransportLifecycleError`,code:t,transportName:e})}var b=class{name=`ws`;lifecycle=Object.freeze({kind:`service`});defaultEnabled=!0;optionsSchema={port:{type:`number`,description:`WebSocket server port`,default:7070},maxRetries:{type:`number`,description:`Port retry attempts when port is occupied`,default:20}};session=null;stopFn=null;state=`detached`;startOperation;startCancelled=!1;port;maxRetries;token;allowedHosts;allowedOrigins;handlerOptions;channels=new o;resolvedPort;constructor(e={}){this.port=e.port??7070,this.maxRetries=e.maxRetries??20;let t=p(e);t.token!==null&&(this.token=t.token),this.allowedHosts=new Set(e.allowedHosts??[]),this.allowedOrigins=new Set(e.allowedOrigins??[]),this.handlerOptions=_(e)}attach(e){this.session=e,this.state=`attached`}registerChannel(e){return this.channels.registerChannel(e)}get boundPort(){return this.resolvedPort}get resolvedToken(){return this.token}async start(){if(!this.session)throw y(this.name,`not-attached`);if(this.state===`starting`||this.state===`ready`||this.state===`stopping`)throw y(this.name,`already-started`);this.state=`starting`,this.startCancelled=!1;let e=this.bindWithRetry(this.session,this.port,this.maxRetries);this.startOperation=e;let t;try{t=await e}catch(t){throw this.state===`starting`&&this.startOperation===e&&(this.startOperation=void 0,this.state=this.session?`attached`:`detached`),t}if(this.startCancelled||this.startOperation!==e)throw await t.stop(),this.state=this.session?`attached`:`detached`,Error(`WsTransport startup was stopped.`);this.stopFn=t.stop,this.resolvedPort=t.port,this.startOperation=void 0,this.state=`ready`}async stop(){if(this.state===`starting`){this.state=`stopping`,this.startCancelled=!0;try{await this.startOperation}catch{}}await this.stopFn?.(),this.stopFn=null,this.startOperation=void 0,this.resolvedPort=void 0,this.session=null,this.state=`detached`}configure(e){if(this.state!==`detached`&&this.state!==`attached`)throw y(this.name,`already-started`);if(!this.validateOptions(e))throw TypeError(`WsTransport options are invalid (port 1-65535, maxRetries >= 0).`);typeof e.port==`number`&&(this.port=e.port),typeof e.maxRetries==`number`&&(this.maxRetries=e.maxRetries)}validateOptions(e){return v(e)}bindWithRetry(e,t,n){return this.tryBind(e,t).catch(r=>{if(r.code===`EADDRINUSE`&&n>0)return this.bindWithRetry(e,t+1,n-1);throw r})}tryBind(r,i){return new Promise((a,o)=>{let s=(0,t.createServer)((e,t)=>{t.writeHead(400).end(`WebSocket endpoint`)});s.on(`error`,e=>{s.close(),o(e)});let c=this.token,p=this.allowedHosts,g=this.allowedOrigins,_=this.channels;s.listen(i,`127.0.0.1`,()=>{let t=new n.WebSocketServer({server:s,verifyClient:(e,t)=>{if(!l(e.req,p)){t(!1,403,`Forbidden host`);return}if(!u(e.req,g)){t(!1,403,`Forbidden origin`);return}t(!0)}});t.on(`connection`,(t,n)=>{if(c!==void 0&&!d(c,f(n))){t.close(1008,`unauthorized`);return}let i=new h(t),a=(0,e.createSessionMessageHandler)({session:r,deliver:i.deliver,...this.handlerOptions});i.bindProtocolCleanup(a.cleanup),i.bindSinkDetach(_.addSink(e=>i.deliverBinary(e))),t.on(`message`,(e,t)=>{if(!t){a.onMessage(String(e));return}let n=_.receive(m(e));n.ok||i.deliver({type:`protocol_error`,message:n.error})}),t.on(`close`,i.close),t.on(`error`,i.close),i.deliver({type:`messages`,messages:r.getMessages()}),i.deliver({type:`execution_workspace_event`,snapshot:r.getExecutionWorkspaceSnapshot()})}),a({port:i,stop:()=>new Promise(e=>{for(let e of t.clients)e.close(1001,`server shutting down`);let n=setTimeout(()=>{for(let e of t.clients)e.terminate()},5e3);t.close(()=>{clearTimeout(n),s.close(()=>e())})})})})})}};exports.PayloadChannelRegistry=o,exports.WsTransport=b,exports.createWsTransport=a;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { TUsageSurface } from "@robota-sdk/agent-interface-analytics";
|
|
2
|
+
import { TDriverId } from "@robota-sdk/agent-interface-session";
|
|
3
|
+
import { IChannelDescriptor, IConfigurableTransport, IPayloadChannel, IPayloadChannelHost, ITransportAdapter, TChannelEventMap, TChannelReceiveResult } from "@robota-sdk/agent-interface-transport";
|
|
4
|
+
import { IProtocolSession, ISessionMessageHandlerOptions, TServerMessage } from "@robota-sdk/agent-transport";
|
|
5
|
+
import { TUniversalValue } from "@robota-sdk/agent-core";
|
|
6
|
+
//#region src/ws-transport.d.ts
|
|
7
|
+
interface IWsTransportOptions {
|
|
8
|
+
/** Send a JSON message to the connected WebSocket client. */
|
|
9
|
+
send: (message: TServerMessage) => void;
|
|
10
|
+
/** Owning socket lifecycle callback for outbound session-event delivery failures. */
|
|
11
|
+
onDeliveryError?: (error: Error, event: string) => void;
|
|
12
|
+
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
13
|
+
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
14
|
+
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
15
|
+
driverId?: TDriverId;
|
|
16
|
+
surface?: TUsageSurface;
|
|
17
|
+
}
|
|
18
|
+
interface IWsTransport extends ITransportAdapter<IProtocolSession> {
|
|
19
|
+
onMessage: ((data: string) => void) | null;
|
|
20
|
+
}
|
|
21
|
+
declare function createWsTransport(options: IWsTransportOptions): IWsTransport;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/ws-transport-config.d.ts
|
|
24
|
+
interface IWsTransportConfig {
|
|
25
|
+
/** Trusted server-assigned identity for turns submitted over this product surface. */
|
|
26
|
+
driverId?: TDriverId;
|
|
27
|
+
/** Trusted product surface, independent from the connection's driver identity. */
|
|
28
|
+
surface?: TUsageSurface;
|
|
29
|
+
/** Host-owned cross-session usage read model, forwarded unchanged to the protocol handler. */
|
|
30
|
+
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
31
|
+
/** Host-owned current-session trace/cost producer, forwarded unchanged to the protocol handler. */
|
|
32
|
+
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
33
|
+
/** Host-owned stored-session trace/cost producer for personal-usage drill-down. */
|
|
34
|
+
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
35
|
+
port?: number;
|
|
36
|
+
maxRetries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* OPTIONAL explicit loopback auth token. When set, every connection MUST present a matching token
|
|
39
|
+
* (query param `?token=` or the `Sec-WebSocket-Protocol` subprotocol) or the socket is closed BEFORE any
|
|
40
|
+
* session data is emitted (GUI-002; the GUI sidecar sets `ROBOTA_WS_TOKEN`). SEC-001: when this is unset
|
|
41
|
+
* AND `open` is not `true`, the transport AUTO-MINTS a random per-launch token (secure by default) —
|
|
42
|
+
* `resolvedToken` exposes it so the surface can deliver it to the co-located client (a `0600` connection
|
|
43
|
+
* file / the served monitor's injected `ws-url`). An explicit token here wins over the auto-mint — but NOT
|
|
44
|
+
* over `open`: SEC-008 makes a token together with `open: true` a contradiction that throws, rather
|
|
45
|
+
* than a precedence one side quietly wins.
|
|
46
|
+
*/
|
|
47
|
+
token?: string;
|
|
48
|
+
/**
|
|
49
|
+
* SEC-001 discouraged opt-out: when `true`, run WITHOUT auth (no token, no auto-mint) — the pre-SEC-001
|
|
50
|
+
* open loopback behavior. NOT RECOMMENDED (any local process or browser page can then drive+authorize the
|
|
51
|
+
* session); mirrors Jupyter's `c.ServerApp.token = ''`.
|
|
52
|
+
*
|
|
53
|
+
* SEC-008: `open: true` together with a non-empty `token` is REJECTED, not resolved by precedence. The
|
|
54
|
+
* two ask for opposite things, and picking a winner silently is how a caller ends up with the admission
|
|
55
|
+
* it did not choose. `resolveAdmission` decides that for every transport, so they cannot differ.
|
|
56
|
+
*/
|
|
57
|
+
open?: boolean;
|
|
58
|
+
/** SEC-008: why running with no credential is correct here. Required when `open` is true. */
|
|
59
|
+
openReason?: string;
|
|
60
|
+
/**
|
|
61
|
+
* SEC-001 defense-in-depth: extra host names (beyond `localhost`/`127.0.0.1`/`::1`) accepted in the
|
|
62
|
+
* upgrade `Host` header. The `Host` allow-list closes DNS-rebinding independently of the token.
|
|
63
|
+
*/
|
|
64
|
+
allowedHosts?: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* SEC-001 defense-in-depth: extra browser `Origin`s (beyond loopback) accepted on the upgrade — e.g. the
|
|
67
|
+
* `apps/agent-web` app origin. A browser sends an unforgeable `Origin`; a non-browser client omits it (and
|
|
68
|
+
* is gated by the token instead). Closes the "any web page in any browser" hole before history is emitted.
|
|
69
|
+
*/
|
|
70
|
+
allowedOrigins?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/ws-transport-configurable.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* TRANS-001: the WS transport is a payload-agnostic CARRIER that routes by WebSocket frame opcode —
|
|
76
|
+
* TEXT frames go to the text-agent protocol profile (`createSessionMessageHandler`), BINARY frames go to the
|
|
77
|
+
* consumer-declared channels. The two profiles share one connection and never constrain each other.
|
|
78
|
+
*/
|
|
79
|
+
declare class WsTransport implements IConfigurableTransport<IProtocolSession>, IPayloadChannelHost {
|
|
80
|
+
readonly name = "ws";
|
|
81
|
+
readonly lifecycle: Readonly<{
|
|
82
|
+
kind: "service";
|
|
83
|
+
}>;
|
|
84
|
+
readonly defaultEnabled = true;
|
|
85
|
+
readonly optionsSchema: {
|
|
86
|
+
port: {
|
|
87
|
+
type: string;
|
|
88
|
+
description: string;
|
|
89
|
+
default: number;
|
|
90
|
+
};
|
|
91
|
+
maxRetries: {
|
|
92
|
+
type: string;
|
|
93
|
+
description: string;
|
|
94
|
+
default: number;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
private session;
|
|
98
|
+
private stopFn;
|
|
99
|
+
private state;
|
|
100
|
+
private startOperation;
|
|
101
|
+
private startCancelled;
|
|
102
|
+
private port;
|
|
103
|
+
private maxRetries;
|
|
104
|
+
private readonly token?;
|
|
105
|
+
private readonly allowedHosts;
|
|
106
|
+
private readonly allowedOrigins;
|
|
107
|
+
private readonly handlerOptions;
|
|
108
|
+
private readonly channels;
|
|
109
|
+
private resolvedPort?;
|
|
110
|
+
constructor(config?: IWsTransportConfig);
|
|
111
|
+
attach(session: IProtocolSession): void;
|
|
112
|
+
/**
|
|
113
|
+
* TRANS-001: declare an app-level channel carried on this transport's connections. The channel's
|
|
114
|
+
* events and opaque binary frames ride alongside the text-agent protocol on the SAME socket — the
|
|
115
|
+
* transport never inspects the payloads. Register before or after `start()`; a channel opened
|
|
116
|
+
* later is served by every already-connected client.
|
|
117
|
+
*/
|
|
118
|
+
registerChannel<TEvents extends TChannelEventMap>(descriptor: IChannelDescriptor<TEvents>): IPayloadChannel<TEvents>;
|
|
119
|
+
/** Actually-bound port after start; undefined before start. */
|
|
120
|
+
get boundPort(): number | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* SEC-001: the resolved auth token (explicit or auto-minted), or `undefined` in the discouraged `open`
|
|
123
|
+
* mode. The surface reads this to deliver the token to the co-located client (a `0600` connection file /
|
|
124
|
+
* the served monitor's injected `ws-url`). Never logged by the transport itself.
|
|
125
|
+
*/
|
|
126
|
+
get resolvedToken(): string | undefined;
|
|
127
|
+
start(): Promise<void>;
|
|
128
|
+
stop(): Promise<void>;
|
|
129
|
+
/** TRANS-002 (issue #2480): persisted `port`/`maxRetries` reach the server through here, before start. */
|
|
130
|
+
configure(options: Record<string, TUniversalValue>): void;
|
|
131
|
+
validateOptions(options: Record<string, TUniversalValue>): boolean;
|
|
132
|
+
private bindWithRetry;
|
|
133
|
+
private tryBind;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/payload-channels.d.ts
|
|
137
|
+
/** Delivers an encoded frame to one attached connection. */
|
|
138
|
+
type TChannelSink = (frame: Uint8Array) => void;
|
|
139
|
+
declare class PayloadChannelRegistry implements IPayloadChannelHost {
|
|
140
|
+
private readonly channels;
|
|
141
|
+
private readonly sinks;
|
|
142
|
+
/**
|
|
143
|
+
* Attach a connection. Every frame a channel sends is fanned out to all attached sinks; the
|
|
144
|
+
* returned function detaches (call it on socket close).
|
|
145
|
+
*/
|
|
146
|
+
addSink(sink: TChannelSink): () => void;
|
|
147
|
+
registerChannel<TEvents extends TChannelEventMap>(descriptor: IChannelDescriptor<TEvents>): IPayloadChannel<TEvents>;
|
|
148
|
+
/**
|
|
149
|
+
* Route one inbound encoded frame to its channel's handlers. The result states exactly what
|
|
150
|
+
* happened so the carrier can answer the peer (a `protocol_error`) instead of dropping it.
|
|
151
|
+
*/
|
|
152
|
+
receive(bytes: Uint8Array): TChannelReceiveResult;
|
|
153
|
+
/** Fan one encoded frame out to every attached connection. */
|
|
154
|
+
private broadcast;
|
|
155
|
+
/** Assert the channel is still open before any send/subscribe. */
|
|
156
|
+
private static assertOpen;
|
|
157
|
+
private createHandle;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { type IWsTransport, type IWsTransportConfig, type IWsTransportOptions, PayloadChannelRegistry, type TChannelSink, WsTransport, createWsTransport };
|
|
161
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/ws-transport.ts","../../src/ws-transport-config.ts","../../src/ws-transport-configurable.ts","../../src/payload-channels.ts"],"mappings":";;;;;;UAqBiB;;EAEf,OAAO,SAAS;;EAEhB,mBAAmB,OAAO,OAAO;EACjC,wBAAwB;EACxB,gBAAgB;EAChB,6BAA6B;EAC7B,WAAW;EACX,UAAU;;UAGK,qBAAqB,kBAAkB;EACtD,aAAa;;iBAGC,kBAAkB,SAAS,sBAAsB;;;UCpBhD;;EAEf,WAAW;;EAEX,UAAU;;EAEV,wBAAwB;;EAExB,gBAAgB;;EAEhB,6BAA6B;EAC7B;EACA;;;;;;;;;;;EAWA;;;;;;;;;;EAUA;;EAEA;;;;;EAKA;;;;;;EAMA;;;;;;;;;cCVW,uBACA,uBAAuB,mBAAmB;WAE5C;WACA,WAAS;;;WACT;WACA;;;;;;;;;;;;UASD;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;UACT;EAEI,YAAA,SAAQ;EAYpB,OAAO,SAAS;;;;;;;EAWhB,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;MAKf;;;;;;MASA;EAIE,SAAS;EA8BT,QAAQ;;EAmBd,UAAU,SAAS,eAAe;EAWlC,gBAAgB,SAAS,eAAe;UAIhC;UAYA;;;;;KC1KE,gBAAgB,OAAO;cAqBtB,kCAAkC;mBAC5B;mBACA;;;;;EAMjB,QAAQ,MAAM;EAOd,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;;;;EAoBnB,QAAQ,OAAO,aAAa;;UAmCpB;;iBAKO;UAIP"}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,187 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IProtocolSession, ISessionMessageHandlerOptions, TServerMessage } from "@robota-sdk/agent-transport";
|
|
2
|
+
import { TUsageSurface } from "@robota-sdk/agent-interface-analytics";
|
|
3
|
+
import { TDriverId } from "@robota-sdk/agent-interface-session";
|
|
4
|
+
import { IChannelDescriptor, IConfigurableTransport, IPayloadChannel, IPayloadChannelHost, ITransportAdapter, TChannelEventMap, TChannelReceiveResult } from "@robota-sdk/agent-interface-transport";
|
|
2
5
|
import { TUniversalValue } from "@robota-sdk/agent-core";
|
|
3
|
-
|
|
4
|
-
//#region src/ws-protocol.d.ts
|
|
5
|
-
type TBackgroundControlAction = 'cancel' | 'close' | 'send';
|
|
6
|
-
/** Inbound message from client to server. */
|
|
7
|
-
type TClientMessage = {
|
|
8
|
-
type: 'submit';
|
|
9
|
-
prompt: string;
|
|
10
|
-
} | {
|
|
11
|
-
type: 'command';
|
|
12
|
-
name: string;
|
|
13
|
-
args?: string;
|
|
14
|
-
} | {
|
|
15
|
-
type: 'abort';
|
|
16
|
-
} | {
|
|
17
|
-
type: 'cancel-queue';
|
|
18
|
-
} | {
|
|
19
|
-
type: 'get-messages';
|
|
20
|
-
} | {
|
|
21
|
-
type: 'get-context';
|
|
22
|
-
} | {
|
|
23
|
-
type: 'get-executing';
|
|
24
|
-
} | {
|
|
25
|
-
type: 'get-pending';
|
|
26
|
-
} | {
|
|
27
|
-
type: 'get-execution-workspace';
|
|
28
|
-
} | {
|
|
29
|
-
type: 'get-background-tasks';
|
|
30
|
-
filter?: IBackgroundTaskListFilter;
|
|
31
|
-
} | {
|
|
32
|
-
type: 'get-background-task';
|
|
33
|
-
taskId: string;
|
|
34
|
-
} | {
|
|
35
|
-
type: 'get-background-job-groups';
|
|
36
|
-
} | {
|
|
37
|
-
type: 'get-background-job-group';
|
|
38
|
-
groupId: string;
|
|
39
|
-
} | {
|
|
40
|
-
type: 'wait-background-job-group';
|
|
41
|
-
groupId: string;
|
|
42
|
-
} | {
|
|
43
|
-
type: 'cancel-background-task';
|
|
44
|
-
taskId: string;
|
|
45
|
-
reason?: string;
|
|
46
|
-
} | {
|
|
47
|
-
type: 'close-background-task';
|
|
48
|
-
taskId: string;
|
|
49
|
-
} | {
|
|
50
|
-
type: 'send-background-task';
|
|
51
|
-
taskId: string;
|
|
52
|
-
input: IBackgroundTaskInput;
|
|
53
|
-
} | {
|
|
54
|
-
type: 'read-background-task-log';
|
|
55
|
-
taskId: string;
|
|
56
|
-
cursor?: IBackgroundTaskLogCursor;
|
|
57
|
-
};
|
|
58
|
-
/** Outbound message from server to client. */
|
|
59
|
-
type TServerMessage = {
|
|
60
|
-
type: 'text_delta';
|
|
61
|
-
delta: string;
|
|
62
|
-
} | {
|
|
63
|
-
type: 'user_message';
|
|
64
|
-
content: string;
|
|
65
|
-
} | {
|
|
66
|
-
type: 'tool_start';
|
|
67
|
-
state: IToolState;
|
|
68
|
-
} | {
|
|
69
|
-
type: 'tool_end';
|
|
70
|
-
state: IToolState;
|
|
71
|
-
} | {
|
|
72
|
-
type: 'thinking';
|
|
73
|
-
isThinking: boolean;
|
|
74
|
-
} | {
|
|
75
|
-
type: 'complete';
|
|
76
|
-
result: IExecutionResult;
|
|
77
|
-
} | {
|
|
78
|
-
type: 'interrupted';
|
|
79
|
-
result: IExecutionResult;
|
|
80
|
-
} | {
|
|
81
|
-
type: 'error';
|
|
82
|
-
message: string;
|
|
83
|
-
} | {
|
|
84
|
-
type: 'command_result';
|
|
85
|
-
name: string;
|
|
86
|
-
message: string;
|
|
87
|
-
success: boolean;
|
|
88
|
-
data?: ICommandResult['data'];
|
|
89
|
-
} | {
|
|
90
|
-
type: 'messages';
|
|
91
|
-
messages: ReturnType<IInteractiveSession['getMessages']>;
|
|
92
|
-
} | {
|
|
93
|
-
type: 'context';
|
|
94
|
-
state: ReturnType<IInteractiveSession['getContextState']>;
|
|
95
|
-
} | {
|
|
96
|
-
type: 'executing';
|
|
97
|
-
executing: boolean;
|
|
98
|
-
} | {
|
|
99
|
-
type: 'pending';
|
|
100
|
-
pending: string | null;
|
|
101
|
-
} | {
|
|
102
|
-
type: 'execution_workspace_event';
|
|
103
|
-
snapshot: IExecutionWorkspaceSnapshot;
|
|
104
|
-
} | {
|
|
105
|
-
type: 'background_task_event';
|
|
106
|
-
event: TBackgroundTaskEvent;
|
|
107
|
-
} | {
|
|
108
|
-
type: 'background_job_group_event';
|
|
109
|
-
event: TBackgroundJobGroupEvent;
|
|
110
|
-
} | {
|
|
111
|
-
type: 'background_tasks';
|
|
112
|
-
tasks: IBackgroundTaskState[];
|
|
113
|
-
} | {
|
|
114
|
-
type: 'background_task';
|
|
115
|
-
taskId: string;
|
|
116
|
-
task: IBackgroundTaskState | null;
|
|
117
|
-
} | {
|
|
118
|
-
type: 'background_job_groups';
|
|
119
|
-
groups: IBackgroundJobGroupState[];
|
|
120
|
-
} | {
|
|
121
|
-
type: 'background_job_group';
|
|
122
|
-
groupId: string;
|
|
123
|
-
group: IBackgroundJobGroupState | null;
|
|
124
|
-
} | {
|
|
125
|
-
type: 'background_task_log';
|
|
126
|
-
taskId: string;
|
|
127
|
-
page: IBackgroundTaskLogPage;
|
|
128
|
-
} | {
|
|
129
|
-
type: 'background_task_control_result';
|
|
130
|
-
action: TBackgroundControlAction;
|
|
131
|
-
taskId: string;
|
|
132
|
-
success: boolean;
|
|
133
|
-
message?: string;
|
|
134
|
-
} | {
|
|
135
|
-
type: 'protocol_error';
|
|
136
|
-
message: string;
|
|
137
|
-
};
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/ws-handler.d.ts
|
|
140
|
-
interface IWsHandlerOptions {
|
|
141
|
-
/** IInteractiveSession to expose. */
|
|
142
|
-
session: IInteractiveSession;
|
|
143
|
-
/** Send a JSON message to the client. */
|
|
144
|
-
send: (message: TServerMessage) => void;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Create a WebSocket message handler for an IInteractiveSession.
|
|
148
|
-
*
|
|
149
|
-
* Returns:
|
|
150
|
-
* - `onMessage(data)`: call this when the WebSocket receives a message
|
|
151
|
-
* - `cleanup()`: call this when the WebSocket disconnects
|
|
152
|
-
*
|
|
153
|
-
* Usage:
|
|
154
|
-
* ```typescript
|
|
155
|
-
* const { onMessage, cleanup } = createWsHandler({
|
|
156
|
-
* session: interactiveSession,
|
|
157
|
-
* send: (msg) => ws.send(JSON.stringify(msg)),
|
|
158
|
-
* });
|
|
159
|
-
*
|
|
160
|
-
* ws.on('message', (data) => onMessage(String(data)));
|
|
161
|
-
* ws.on('close', cleanup);
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
declare function createWsHandler(options: IWsHandlerOptions): {
|
|
165
|
-
onMessage: (data: string) => void;
|
|
166
|
-
cleanup: () => void;
|
|
167
|
-
};
|
|
168
|
-
//#endregion
|
|
169
6
|
//#region src/ws-transport.d.ts
|
|
170
7
|
interface IWsTransportOptions {
|
|
171
8
|
/** Send a JSON message to the connected WebSocket client. */
|
|
172
9
|
send: (message: TServerMessage) => void;
|
|
10
|
+
/** Owning socket lifecycle callback for outbound session-event delivery failures. */
|
|
11
|
+
onDeliveryError?: (error: Error, event: string) => void;
|
|
12
|
+
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
13
|
+
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
14
|
+
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
15
|
+
driverId?: TDriverId;
|
|
16
|
+
surface?: TUsageSurface;
|
|
173
17
|
}
|
|
174
|
-
|
|
18
|
+
interface IWsTransport extends ITransportAdapter<IProtocolSession> {
|
|
175
19
|
onMessage: ((data: string) => void) | null;
|
|
176
|
-
}
|
|
20
|
+
}
|
|
21
|
+
declare function createWsTransport(options: IWsTransportOptions): IWsTransport;
|
|
177
22
|
//#endregion
|
|
178
|
-
//#region src/ws-transport-
|
|
23
|
+
//#region src/ws-transport-config.d.ts
|
|
179
24
|
interface IWsTransportConfig {
|
|
25
|
+
/** Trusted server-assigned identity for turns submitted over this product surface. */
|
|
26
|
+
driverId?: TDriverId;
|
|
27
|
+
/** Trusted product surface, independent from the connection's driver identity. */
|
|
28
|
+
surface?: TUsageSurface;
|
|
29
|
+
/** Host-owned cross-session usage read model, forwarded unchanged to the protocol handler. */
|
|
30
|
+
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
31
|
+
/** Host-owned current-session trace/cost producer, forwarded unchanged to the protocol handler. */
|
|
32
|
+
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
33
|
+
/** Host-owned stored-session trace/cost producer for personal-usage drill-down. */
|
|
34
|
+
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
180
35
|
port?: number;
|
|
181
36
|
maxRetries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* OPTIONAL explicit loopback auth token. When set, every connection MUST present a matching token
|
|
39
|
+
* (query param `?token=` or the `Sec-WebSocket-Protocol` subprotocol) or the socket is closed BEFORE any
|
|
40
|
+
* session data is emitted (GUI-002; the GUI sidecar sets `ROBOTA_WS_TOKEN`). SEC-001: when this is unset
|
|
41
|
+
* AND `open` is not `true`, the transport AUTO-MINTS a random per-launch token (secure by default) —
|
|
42
|
+
* `resolvedToken` exposes it so the surface can deliver it to the co-located client (a `0600` connection
|
|
43
|
+
* file / the served monitor's injected `ws-url`). An explicit token here wins over the auto-mint — but NOT
|
|
44
|
+
* over `open`: SEC-008 makes a token together with `open: true` a contradiction that throws, rather
|
|
45
|
+
* than a precedence one side quietly wins.
|
|
46
|
+
*/
|
|
47
|
+
token?: string;
|
|
48
|
+
/**
|
|
49
|
+
* SEC-001 discouraged opt-out: when `true`, run WITHOUT auth (no token, no auto-mint) — the pre-SEC-001
|
|
50
|
+
* open loopback behavior. NOT RECOMMENDED (any local process or browser page can then drive+authorize the
|
|
51
|
+
* session); mirrors Jupyter's `c.ServerApp.token = ''`.
|
|
52
|
+
*
|
|
53
|
+
* SEC-008: `open: true` together with a non-empty `token` is REJECTED, not resolved by precedence. The
|
|
54
|
+
* two ask for opposite things, and picking a winner silently is how a caller ends up with the admission
|
|
55
|
+
* it did not choose. `resolveAdmission` decides that for every transport, so they cannot differ.
|
|
56
|
+
*/
|
|
57
|
+
open?: boolean;
|
|
58
|
+
/** SEC-008: why running with no credential is correct here. Required when `open` is true. */
|
|
59
|
+
openReason?: string;
|
|
60
|
+
/**
|
|
61
|
+
* SEC-001 defense-in-depth: extra host names (beyond `localhost`/`127.0.0.1`/`::1`) accepted in the
|
|
62
|
+
* upgrade `Host` header. The `Host` allow-list closes DNS-rebinding independently of the token.
|
|
63
|
+
*/
|
|
64
|
+
allowedHosts?: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* SEC-001 defense-in-depth: extra browser `Origin`s (beyond loopback) accepted on the upgrade — e.g. the
|
|
67
|
+
* `apps/agent-web` app origin. A browser sends an unforgeable `Origin`; a non-browser client omits it (and
|
|
68
|
+
* is gated by the token instead). Closes the "any web page in any browser" hole before history is emitted.
|
|
69
|
+
*/
|
|
70
|
+
allowedOrigins?: readonly string[];
|
|
182
71
|
}
|
|
183
|
-
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/ws-transport-configurable.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* TRANS-001: the WS transport is a payload-agnostic CARRIER that routes by WebSocket frame opcode —
|
|
76
|
+
* TEXT frames go to the text-agent protocol profile (`createSessionMessageHandler`), BINARY frames go to the
|
|
77
|
+
* consumer-declared channels. The two profiles share one connection and never constrain each other.
|
|
78
|
+
*/
|
|
79
|
+
declare class WsTransport implements IConfigurableTransport<IProtocolSession>, IPayloadChannelHost {
|
|
184
80
|
readonly name = "ws";
|
|
81
|
+
readonly lifecycle: Readonly<{
|
|
82
|
+
kind: "service";
|
|
83
|
+
}>;
|
|
185
84
|
readonly defaultEnabled = true;
|
|
186
85
|
readonly optionsSchema: {
|
|
187
86
|
port: {
|
|
@@ -197,16 +96,66 @@ declare class WsTransport implements IConfigurableTransport<IInteractiveSession>
|
|
|
197
96
|
};
|
|
198
97
|
private session;
|
|
199
98
|
private stopFn;
|
|
200
|
-
private
|
|
201
|
-
private
|
|
99
|
+
private state;
|
|
100
|
+
private startOperation;
|
|
101
|
+
private startCancelled;
|
|
102
|
+
private port;
|
|
103
|
+
private maxRetries;
|
|
104
|
+
private readonly token?;
|
|
105
|
+
private readonly allowedHosts;
|
|
106
|
+
private readonly allowedOrigins;
|
|
107
|
+
private readonly handlerOptions;
|
|
108
|
+
private readonly channels;
|
|
109
|
+
private resolvedPort?;
|
|
202
110
|
constructor(config?: IWsTransportConfig);
|
|
203
|
-
attach(session:
|
|
111
|
+
attach(session: IProtocolSession): void;
|
|
112
|
+
/**
|
|
113
|
+
* TRANS-001: declare an app-level channel carried on this transport's connections. The channel's
|
|
114
|
+
* events and opaque binary frames ride alongside the text-agent protocol on the SAME socket — the
|
|
115
|
+
* transport never inspects the payloads. Register before or after `start()`; a channel opened
|
|
116
|
+
* later is served by every already-connected client.
|
|
117
|
+
*/
|
|
118
|
+
registerChannel<TEvents extends TChannelEventMap>(descriptor: IChannelDescriptor<TEvents>): IPayloadChannel<TEvents>;
|
|
119
|
+
/** Actually-bound port after start; undefined before start. */
|
|
120
|
+
get boundPort(): number | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* SEC-001: the resolved auth token (explicit or auto-minted), or `undefined` in the discouraged `open`
|
|
123
|
+
* mode. The surface reads this to deliver the token to the co-located client (a `0600` connection file /
|
|
124
|
+
* the served monitor's injected `ws-url`). Never logged by the transport itself.
|
|
125
|
+
*/
|
|
126
|
+
get resolvedToken(): string | undefined;
|
|
204
127
|
start(): Promise<void>;
|
|
205
128
|
stop(): Promise<void>;
|
|
129
|
+
/** TRANS-002 (issue #2480): persisted `port`/`maxRetries` reach the server through here, before start. */
|
|
130
|
+
configure(options: Record<string, TUniversalValue>): void;
|
|
206
131
|
validateOptions(options: Record<string, TUniversalValue>): boolean;
|
|
207
132
|
private bindWithRetry;
|
|
208
133
|
private tryBind;
|
|
209
134
|
}
|
|
210
135
|
//#endregion
|
|
211
|
-
|
|
136
|
+
//#region src/payload-channels.d.ts
|
|
137
|
+
/** Delivers an encoded frame to one attached connection. */
|
|
138
|
+
type TChannelSink = (frame: Uint8Array) => void;
|
|
139
|
+
declare class PayloadChannelRegistry implements IPayloadChannelHost {
|
|
140
|
+
private readonly channels;
|
|
141
|
+
private readonly sinks;
|
|
142
|
+
/**
|
|
143
|
+
* Attach a connection. Every frame a channel sends is fanned out to all attached sinks; the
|
|
144
|
+
* returned function detaches (call it on socket close).
|
|
145
|
+
*/
|
|
146
|
+
addSink(sink: TChannelSink): () => void;
|
|
147
|
+
registerChannel<TEvents extends TChannelEventMap>(descriptor: IChannelDescriptor<TEvents>): IPayloadChannel<TEvents>;
|
|
148
|
+
/**
|
|
149
|
+
* Route one inbound encoded frame to its channel's handlers. The result states exactly what
|
|
150
|
+
* happened so the carrier can answer the peer (a `protocol_error`) instead of dropping it.
|
|
151
|
+
*/
|
|
152
|
+
receive(bytes: Uint8Array): TChannelReceiveResult;
|
|
153
|
+
/** Fan one encoded frame out to every attached connection. */
|
|
154
|
+
private broadcast;
|
|
155
|
+
/** Assert the channel is still open before any send/subscribe. */
|
|
156
|
+
private static assertOpen;
|
|
157
|
+
private createHandle;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { type IWsTransport, type IWsTransportConfig, type IWsTransportOptions, PayloadChannelRegistry, type TChannelSink, WsTransport, createWsTransport };
|
|
212
161
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/ws-
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/ws-transport.ts","../../src/ws-transport-config.ts","../../src/ws-transport-configurable.ts","../../src/payload-channels.ts"],"mappings":";;;;;;UAqBiB;;EAEf,OAAO,SAAS;;EAEhB,mBAAmB,OAAO,OAAO;EACjC,wBAAwB;EACxB,gBAAgB;EAChB,6BAA6B;EAC7B,WAAW;EACX,UAAU;;UAGK,qBAAqB,kBAAkB;EACtD,aAAa;;iBAGC,kBAAkB,SAAS,sBAAsB;;;UCpBhD;;EAEf,WAAW;;EAEX,UAAU;;EAEV,wBAAwB;;EAExB,gBAAgB;;EAEhB,6BAA6B;EAC7B;EACA;;;;;;;;;;;EAWA;;;;;;;;;;EAUA;;EAEA;;;;;EAKA;;;;;;EAMA;;;;;;;;;cCVW,uBACA,uBAAuB,mBAAmB;WAE5C;WACA,WAAS;;;WACT;WACA;;;;;;;;;;;;UASD;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;UACT;EAEI,YAAA,SAAQ;EAYpB,OAAO,SAAS;;;;;;;EAWhB,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;MAKf;;;;;;MASA;EAIE,SAAS;EA8BT,QAAQ;;EAmBd,UAAU,SAAS,eAAe;EAWlC,gBAAgB,SAAS,eAAe;UAIhC;UAYA;;;;;KC1KE,gBAAgB,OAAO;cAqBtB,kCAAkC;mBAC5B;mBACA;;;;;EAMjB,QAAQ,MAAM;EAOd,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;;;;EAoBnB,QAAQ,OAAO,aAAa;;UAmCpB;;iBAKO;UAIP"}
|