@robota-sdk/agent-transport-ws 3.0.0-beta.81 → 3.0.0-beta.83
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/CHANGELOG.md +344 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +17 -1
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +17 -1
- 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 +11 -9
- package/src/__tests__/ws-session-binder.test.ts +269 -0
- package/src/__tests__/ws-session-directory.test.ts +95 -0
- package/src/ws-transport-config.ts +18 -3
- package/src/ws-transport-configurable.ts +35 -7
- package/src/ws-transport.ts +2 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# @robota-sdk/agent-transport-ws
|
|
2
|
+
|
|
3
|
+
## 3.0.0-beta.83
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6ae3f28: A served runtime can list its workspace's sessions, start a new one and switch to another without the
|
|
8
|
+
process or any client connection restarting. The GUI shows them in a sessions sidebar.
|
|
9
|
+
|
|
10
|
+
- `agent-interface-session` is **`major`** for two reasons:
|
|
11
|
+
- It adds `ISessionListing`, `ISessionDirectory` and `ISessionSwitchedEvent`.
|
|
12
|
+
- `IInteractiveSessionEvents` gains a required `session_switched` event. An exhaustive map over the
|
|
13
|
+
event names stops compiling until it classifies the new event.
|
|
14
|
+
- `agent-transport` is **`major`** for the same kind of reason:
|
|
15
|
+
- It adds the wire messages `list-sessions` → `sessions`/`sessions_error`, `new-session`,
|
|
16
|
+
`switch-session`, and the broadcast `session_switched`.
|
|
17
|
+
- It adds a `sessionDirectory` handler option. A refusal to switch comes back as a
|
|
18
|
+
`protocol_error` carrying the reason.
|
|
19
|
+
- An exhaustive map over message types must add the new variants.
|
|
20
|
+
- `agent-transport-ws` passes a configured `sessionDirectory` to every connection.
|
|
21
|
+
- `agent-framework` is **`major` because `IRuntimeHostHandle.session` is now a `SessionSlot`**, not the
|
|
22
|
+
`InteractiveSession`, and `bindTransports` receives the slot.
|
|
23
|
+
- The slot is an `IInteractiveSession` that forwards to the current session. Members outside that
|
|
24
|
+
interface are read through `host.session.current`, which changes on a switch.
|
|
25
|
+
- Adds `InteractiveSession.whenInitialized()`.
|
|
26
|
+
- Exports `listUnreadableSessions`.
|
|
27
|
+
- `agent-ui-web` is **`major`**:
|
|
28
|
+
- `IWsSessionState` gains session listing state and actions.
|
|
29
|
+
- It adds a `SessionSidebar`.
|
|
30
|
+
- `/resume` opens the sidebar.
|
|
31
|
+
- `agent-cli`: `robota --serve` provides the session directory, and refuses a switch that would lose
|
|
32
|
+
work in progress. External-event grants belong to the run: a switch reopens them on the new session,
|
|
33
|
+
as the TUI already does.
|
|
34
|
+
- External-event grant history (spent tokens, rate windows, revocations) belongs to the run. The new
|
|
35
|
+
option `externalEventGrantHistory` (from `createExternalEventGrantHistory()`) is shared by every
|
|
36
|
+
session that a served runtime or the TUI builds. A session switch therefore replays no spent token
|
|
37
|
+
and resets no rate limit. The TUI previously had this gap when it switched sessions.
|
|
38
|
+
|
|
39
|
+
- 57f57f5: A daemon keeps several sessions live, and each client is bound to its own: one client's switch moves
|
|
40
|
+
only that client.
|
|
41
|
+
|
|
42
|
+
- `agent-interface-session`: `ISessionBinder`/`ISessionBinding`, the session-change refusal codes with
|
|
43
|
+
`isSessionChangeRefusal`, and listing rows that may say `live` and `clients`.
|
|
44
|
+
- `agent-framework`: `SessionPool` and `SessionChangeRefusal`.
|
|
45
|
+
- `agent-transport` (**major**): a new server frame, `session_change_failed`, which an exhaustive
|
|
46
|
+
consumer must handle. A refused `new-session` or `switch-session` now answers with it, carrying a
|
|
47
|
+
`code`, instead of `protocol_error`; both requests take an optional `requestId` that it echoes.
|
|
48
|
+
- `agent-transport-ws`: the `sessionBinder` option binds each connection to its own session and
|
|
49
|
+
releases the binding when the connection closes.
|
|
50
|
+
- `agent-cli`: `robota --serve` and the daemon keep up to four sessions live. Each WebSocket client and
|
|
51
|
+
attached terminal is bound to its own session; leaving a busy session is no longer refused, and only
|
|
52
|
+
the last driver of a session with a pending prompt is kept from leaving it. Grants and the supervised
|
|
53
|
+
name stay on the runtime's first session, and its reported activity covers every live session.
|
|
54
|
+
- `agent-ui-web`: a refused new or switch shows the host's reason as a notice (an older host's
|
|
55
|
+
`protocol_error` still does); the session sidebar marks live sessions and counts the other clients
|
|
56
|
+
on each; after a reconnect to a host that keeps sessions live, the GUI returns to the session it was on.
|
|
57
|
+
- `agent-ui-terminal`: an attached terminal shows why a switch was refused, and the picker's switch
|
|
58
|
+
stays pending until the host answers; the session picker marks live sessions and their clients.
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- 57280bf: Every published package now declares `"engines": { "node": ">=22.12.0" }`. Before, 27 of the 38
|
|
63
|
+
packages declared no floor (`agent-core`, `agent-tools` and every provider among them),
|
|
64
|
+
`agent-session` and `agent-file-authority` declared `>=20.19.0`, and the other nine declared
|
|
65
|
+
`>=22.0.0`, so a consumer on Node 20 saw at most a warning from a transitive dependency.
|
|
66
|
+
|
|
67
|
+
Why 22.12: `agent-cli` and `agent-ui-terminal` need Node 22 through `ink` 7, and the CommonJS entries
|
|
68
|
+
of `agent-tools` and its dependents, `agent-transport`/`node` and its dependents, and
|
|
69
|
+
`agent-ui-terminal` `require()` ESM-only dependencies (`p-limit`, `jose`, `chalk`), which Node 22
|
|
70
|
+
supports unflagged only from 22.12. `engines` is advisory unless the consumer enables `engine-strict`.
|
|
71
|
+
|
|
72
|
+
No code changes: `tsdown` now reads `node22.12.0` as its build target from the field.
|
|
73
|
+
|
|
74
|
+
- 18c0d5c: Every published package now exports `./package.json`, so `require('<package>/package.json')` and
|
|
75
|
+
`import('<package>/package.json', { with: { type: 'json' } })` work instead of failing with
|
|
76
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and each tarball now ships the package's `CHANGELOG.md`.
|
|
77
|
+
- Updated dependencies [3c81769]
|
|
78
|
+
- Updated dependencies [724fabb]
|
|
79
|
+
- Updated dependencies [d877de2]
|
|
80
|
+
- Updated dependencies [d61e159]
|
|
81
|
+
- Updated dependencies [bfe8ed5]
|
|
82
|
+
- Updated dependencies [e689c8e]
|
|
83
|
+
- Updated dependencies [7b72344]
|
|
84
|
+
- Updated dependencies [6ae3f28]
|
|
85
|
+
- Updated dependencies [be0e53c]
|
|
86
|
+
- Updated dependencies [57f57f5]
|
|
87
|
+
- Updated dependencies [ba822c1]
|
|
88
|
+
- Updated dependencies [6e6b06b]
|
|
89
|
+
- Updated dependencies [8bd5fac]
|
|
90
|
+
- Updated dependencies [57280bf]
|
|
91
|
+
- Updated dependencies [5033dd9]
|
|
92
|
+
- Updated dependencies [18c0d5c]
|
|
93
|
+
- Updated dependencies [dbd888d]
|
|
94
|
+
- Updated dependencies [1887e54]
|
|
95
|
+
- @robota-sdk/agent-interface-analytics@3.0.0-beta.83
|
|
96
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.83
|
|
97
|
+
- @robota-sdk/agent-transport@3.0.0-beta.83
|
|
98
|
+
- @robota-sdk/agent-core@3.0.0-beta.83
|
|
99
|
+
- @robota-sdk/agent-interface-session@3.0.0-beta.83
|
|
100
|
+
|
|
101
|
+
## 3.0.0-beta.82
|
|
102
|
+
|
|
103
|
+
### Patch Changes
|
|
104
|
+
|
|
105
|
+
- Updated dependencies [c7f9203]
|
|
106
|
+
- @robota-sdk/agent-transport@3.0.0-beta.82
|
|
107
|
+
- @robota-sdk/agent-core@3.0.0-beta.82
|
|
108
|
+
- @robota-sdk/agent-interface-session@3.0.0-beta.82
|
|
109
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.82
|
|
110
|
+
- @robota-sdk/agent-interface-analytics@3.0.0-beta.82
|
|
111
|
+
|
|
112
|
+
## 3.0.0-beta.81
|
|
113
|
+
|
|
114
|
+
### Patch Changes
|
|
115
|
+
|
|
116
|
+
- Updated dependencies [3038eb7]
|
|
117
|
+
- Updated dependencies [02b7452]
|
|
118
|
+
- Updated dependencies [ec5e477]
|
|
119
|
+
- @robota-sdk/agent-core@3.0.0-beta.81
|
|
120
|
+
- @robota-sdk/agent-interface-session@3.0.0-beta.81
|
|
121
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.81
|
|
122
|
+
- @robota-sdk/agent-transport@3.0.0-beta.81
|
|
123
|
+
- @robota-sdk/agent-interface-analytics@3.0.0-beta.81
|
|
124
|
+
|
|
125
|
+
## 3.0.0-beta.80
|
|
126
|
+
|
|
127
|
+
### Major Changes
|
|
128
|
+
|
|
129
|
+
- e82215f: **ARCH-011: replace the ambiguous transport lifecycle stub with executable conformance.**
|
|
130
|
+
|
|
131
|
+
`ITransportAdapter` now requires a frozen `service | runner` lifecycle descriptor. `start()` resolves
|
|
132
|
+
at the concrete transport's documented readiness boundary; start before attach and repeated active
|
|
133
|
+
start reject a stable lifecycle error, repeated stop is safe, and stopped adapters can reattach and
|
|
134
|
+
restart.
|
|
135
|
+
|
|
136
|
+
Runner adapters launch separately and expose a typed terminal outcome through
|
|
137
|
+
`waitForCompletion()`. The registry accepts base adapters, rejects duplicate names, keeps
|
|
138
|
+
configuration as an optional capability, returns complete ordered records whose pending slots become
|
|
139
|
+
registry-owned `abandoned` outcomes on stop/rollback, and exposes a real-runner-only first-failure
|
|
140
|
+
wait. It serializes startup/stop, rejects active restart before mutation, and reverses partial startup
|
|
141
|
+
from the currently failing adapter with typed safe rollback details. Runtime host and serve mode
|
|
142
|
+
propagate real nonzero runner results without treating normal shutdown abandonment as failure.
|
|
143
|
+
|
|
144
|
+
HTTP, MCP, both WebSocket adapters, WebRTC, and headless invoke one shared public conformance kit.
|
|
145
|
+
The former `TuiTransport` export is removed because it ignored the attached session; use `renderApp`
|
|
146
|
+
or `TuiInteractionChannel`, which honestly own their session lifecycle.
|
|
147
|
+
|
|
148
|
+
### Minor Changes
|
|
149
|
+
|
|
150
|
+
- 9db63ee: Add named session capability roles and explicit capability-host queries while preserving the legacy
|
|
151
|
+
`IInteractiveSession` interface shape. HTTP, MCP, protocol, WS, WebRTC, and headless transports now
|
|
152
|
+
declare only the session roles they consume, and the direct aggregate-cast floor is zero.
|
|
153
|
+
- 2ebff01: Emit the complete persisted checkpoint and branch lifecycle, forward plan, context-refresh, and
|
|
154
|
+
branch events through protocol transports, and render deterministic bounded notices in the TUI.
|
|
155
|
+
Transport-owned delivery failures now enter the owning carrier cleanup lifecycle without reversing
|
|
156
|
+
an already-committed session operation.
|
|
157
|
+
- 5c5ff23: TRANS-001: payload-agnostic transport — opaque binary frames + consumer-declared event types
|
|
158
|
+
|
|
159
|
+
The WS transport now carries **arbitrary payloads** alongside the text-agent protocol on one
|
|
160
|
+
connection, instead of forcing every app-level payload through the `text_delta`/`submit` wire
|
|
161
|
+
protocol.
|
|
162
|
+
|
|
163
|
+
- `agent-interface-transport` adds the channel contracts (`IBinaryFrame`, `IChannelEventFrame`,
|
|
164
|
+
`IChannelDescriptor`, `IPayloadChannel`, `IPayloadChannelHost`, `TChannelEventMap`,
|
|
165
|
+
`TChannelFrame`, `TChannelReceiveResult`). Content-neutral carrier mechanics — no payload domain.
|
|
166
|
+
- `agent-transport-protocol` adds the pure channel frame codec (`encodeBinaryFrame`,
|
|
167
|
+
`encodeChannelEventFrame`, `decodeChannelFrame`, `isChannelFrame`). `TClientMessage` /
|
|
168
|
+
`TServerMessage` are unchanged.
|
|
169
|
+
- `agent-transport-ws` becomes a carrier that routes by WebSocket frame opcode — TEXT to the
|
|
170
|
+
text-agent protocol profile, BINARY to consumer-declared channels — and `WsTransport` now
|
|
171
|
+
implements `IPayloadChannelHost` (`registerChannel`). `PayloadChannelRegistry` is exported.
|
|
172
|
+
|
|
173
|
+
Additive only: existing transports, consumers, and the agent wire protocol are untouched.
|
|
174
|
+
|
|
175
|
+
### Patch Changes
|
|
176
|
+
|
|
177
|
+
- 235da81: **BREAKING — ARCH-030: `createWsHandler` takes the carrier's delivery boundary, not a raw `send`.**
|
|
178
|
+
|
|
179
|
+
`createWsHandler` had two outbound semantics on one connection. The session-event fan-out went through
|
|
180
|
+
a guard that reported carrier failures through `onDeliveryError`; every reply to an inbound frame got
|
|
181
|
+
the raw `send`. Eleven reply families were unguarded — five resolving from a Promise continuation, so a
|
|
182
|
+
reply landing after a disconnect escaped as an **unhandled rejection** while the carrier's cleanup was
|
|
183
|
+
never notified, and six synchronous ones that threw into the carrier's inbound listener instead.
|
|
184
|
+
|
|
185
|
+
`IWsHandlerOptions` now takes a single `deliver: TOutboundDeliver` in place of `send` and
|
|
186
|
+
`onDeliveryError`. **The carrier builds the boundary** from its own sink and its own failure policy and
|
|
187
|
+
passes it down — not the reverse, because a protocol layer handed a raw sink so it can hand a wrapper
|
|
188
|
+
back leaves the raw sink reachable, which is how the twelfth reply family gets added unguarded.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// before
|
|
192
|
+
const { onMessage, cleanup } = createWsHandler({
|
|
193
|
+
session,
|
|
194
|
+
send: (msg) => ws.send(JSON.stringify(msg)),
|
|
195
|
+
onDeliveryError: (error) => ws.close(1011, error.message),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// after
|
|
199
|
+
const deliver = createOutboundDelivery(
|
|
200
|
+
(msg) => ws.send(JSON.stringify(msg)),
|
|
201
|
+
(error) => ws.close(1011, error.message),
|
|
202
|
+
);
|
|
203
|
+
const { onMessage, cleanup } = createWsHandler({ session, deliver });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`TOutboundDeliver` is branded and `createOutboundDelivery` is its only producer, so a plain
|
|
207
|
+
`(message: TServerMessage) => void` is refused by the compiler wherever a boundary is required.
|
|
208
|
+
|
|
209
|
+
**The boundary latches:** it reports at most one delivery failure per connection, after which frames are
|
|
210
|
+
dropped without a further report. All three carriers already treated a delivery failure as terminal and
|
|
211
|
+
each had grown its own latch; it belongs upstream of all three. `SessionResumeBridge` builds a fresh
|
|
212
|
+
boundary per `attach`, which is what un-latches the session after a reconnect, and buffers a frame
|
|
213
|
+
before the boundary so a dropped one still replays.
|
|
214
|
+
|
|
215
|
+
**`ISubscribeSessionEventsOptions` is no longer exported** from the package barrel. It is the options bag
|
|
216
|
+
of `subscribeSessionEvents`, which is package-internal, and it was already absent from the SPEC's public
|
|
217
|
+
API table. Its `onDeliveryError` member is gone regardless — carrier-failure containment is the
|
|
218
|
+
boundary's job now.
|
|
219
|
+
|
|
220
|
+
`agent-transport-ws` and `agent-transport-webrtc` are `patch`: `WsSessionDelivery` (whose raw `send` is
|
|
221
|
+
now private, with `deliver` the only public sink) and `PairingGate` are not on their packages' barrels,
|
|
222
|
+
and every barrel export of both packages keeps its signature.
|
|
223
|
+
|
|
224
|
+
- 4f3c075: Assemble complete, verified package generations before switching build output; preserve the previous generation on build failure and pack only verified regular-file images. Include copied CLI web assets in affected-build ordering and artifact transfer. Preserve the CLI version in managed build paths. Public runtime contracts remain compatible (patch).
|
|
225
|
+
- 94cc355: REMOTE-002 Stage A: extract the transport-neutral session bridge + wire protocol into a new
|
|
226
|
+
`@robota-sdk/agent-transport-protocol` package (`createWsHandler`, `TClientMessage`/`TServerMessage`); repoint
|
|
227
|
+
`agent-transport-ws` and `agent-web-ui` at it (no pass-through re-export). Add a new
|
|
228
|
+
`@robota-sdk/agent-transport-webrtc` package: a `WebRtcTransport` (`IConfigurableTransport`, `defaultEnabled:false`)
|
|
229
|
+
that carries an `IInteractiveSession` over an `RTCDataChannel` reusing the shared handler, with a lazily-loaded
|
|
230
|
+
optional `werift` peer dependency that throws an explicit "WebRTC transport unavailable" error on absence (never a
|
|
231
|
+
silent no-op). No user-facing enable path and no auth in Stage A (that lands in Stage B); the transport is proven
|
|
232
|
+
by an in-process loopback data-channel round-trip only.
|
|
233
|
+
|
|
234
|
+
(Bump target corrected during REL-023 triage: `@robota-sdk/agent-web-ui` was dissolved by GUI-006 (#1141, 2026-07-12) before this work was ever published; its protocol-consumer role now lives in `@robota-sdk/agent-transport-webrtc-web`.)
|
|
235
|
+
|
|
236
|
+
- Updated dependencies [7b6234c]
|
|
237
|
+
- Updated dependencies [37b4bd7]
|
|
238
|
+
- Updated dependencies [4eea54b]
|
|
239
|
+
- Updated dependencies [50d2c9f]
|
|
240
|
+
- Updated dependencies [1698be4]
|
|
241
|
+
- Updated dependencies [a5961c9]
|
|
242
|
+
- Updated dependencies [d4189b9]
|
|
243
|
+
- Updated dependencies [9edae52]
|
|
244
|
+
- Updated dependencies [4078a72]
|
|
245
|
+
- Updated dependencies [4c73a0b]
|
|
246
|
+
- Updated dependencies [196a900]
|
|
247
|
+
- Updated dependencies [f336838]
|
|
248
|
+
- Updated dependencies [34e50f0]
|
|
249
|
+
- Updated dependencies [722e88a]
|
|
250
|
+
- Updated dependencies [d23c848]
|
|
251
|
+
- Updated dependencies [4c5148e]
|
|
252
|
+
- Updated dependencies [0116a29]
|
|
253
|
+
- Updated dependencies [fec722f]
|
|
254
|
+
- Updated dependencies [e82215f]
|
|
255
|
+
- Updated dependencies [52b7346]
|
|
256
|
+
- Updated dependencies [9db63ee]
|
|
257
|
+
- Updated dependencies [b078afa]
|
|
258
|
+
- Updated dependencies [2ebff01]
|
|
259
|
+
- Updated dependencies [9db63ee]
|
|
260
|
+
- Updated dependencies [2ebff01]
|
|
261
|
+
- Updated dependencies [2d3b2c0]
|
|
262
|
+
- Updated dependencies [4772067]
|
|
263
|
+
- Updated dependencies [0f98419]
|
|
264
|
+
- Updated dependencies [64ba748]
|
|
265
|
+
- Updated dependencies [9fbab1b]
|
|
266
|
+
- Updated dependencies [d312755]
|
|
267
|
+
- Updated dependencies [a009f5b]
|
|
268
|
+
- Updated dependencies [3244fb8]
|
|
269
|
+
- Updated dependencies [1e3f91a]
|
|
270
|
+
- Updated dependencies [4f3c075]
|
|
271
|
+
- Updated dependencies [475e085]
|
|
272
|
+
- Updated dependencies [e477440]
|
|
273
|
+
- Updated dependencies [9dcb5da]
|
|
274
|
+
- Updated dependencies [a95ca85]
|
|
275
|
+
- Updated dependencies [b6d14ce]
|
|
276
|
+
- Updated dependencies [0382a51]
|
|
277
|
+
- Updated dependencies [93d061d]
|
|
278
|
+
- Updated dependencies [39554a1]
|
|
279
|
+
- Updated dependencies [d28430a]
|
|
280
|
+
- Updated dependencies [7669851]
|
|
281
|
+
- Updated dependencies [4b76cfa]
|
|
282
|
+
- Updated dependencies [07b627f]
|
|
283
|
+
- Updated dependencies [d0de5b2]
|
|
284
|
+
- Updated dependencies [9665c6e]
|
|
285
|
+
- Updated dependencies [44393be]
|
|
286
|
+
- Updated dependencies [c7fa299]
|
|
287
|
+
- Updated dependencies [5134b3b]
|
|
288
|
+
- Updated dependencies [d6b9404]
|
|
289
|
+
- Updated dependencies [5c5ff23]
|
|
290
|
+
- Updated dependencies [9814afc]
|
|
291
|
+
- @robota-sdk/agent-core@3.0.0-beta.80
|
|
292
|
+
- @robota-sdk/agent-interface-session@3.0.0-beta.80
|
|
293
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.80
|
|
294
|
+
- @robota-sdk/agent-transport@3.0.0-beta.80
|
|
295
|
+
- @robota-sdk/agent-interface-analytics@3.0.0-beta.80
|
|
296
|
+
|
|
297
|
+
## 3.0.0-beta.79
|
|
298
|
+
|
|
299
|
+
### Patch Changes
|
|
300
|
+
|
|
301
|
+
- @robota-sdk/agent-core@3.0.0-beta.79
|
|
302
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.79
|
|
303
|
+
|
|
304
|
+
## 3.0.0-beta.78
|
|
305
|
+
|
|
306
|
+
### Patch Changes
|
|
307
|
+
|
|
308
|
+
- Updated dependencies [6f308d1]
|
|
309
|
+
- @robota-sdk/agent-core@3.0.0-beta.78
|
|
310
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.78
|
|
311
|
+
|
|
312
|
+
## 3.0.0-beta.77
|
|
313
|
+
|
|
314
|
+
### Patch Changes
|
|
315
|
+
|
|
316
|
+
- Updated dependencies
|
|
317
|
+
- @robota-sdk/agent-core@3.0.0-beta.77
|
|
318
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.77
|
|
319
|
+
|
|
320
|
+
## 3.0.0-beta.76
|
|
321
|
+
|
|
322
|
+
### Minor Changes
|
|
323
|
+
|
|
324
|
+
- 9df3a88: Split the consolidated `@robota-sdk/agent-transport` package into per-concern transport packages (DQ-AUDIT-005) so unrelated heavy dependencies (React/Ink, ws, Hono, MCP SDK) no longer share one publishable unit and are not dragged into non-TUI consumers' graphs:
|
|
325
|
+
|
|
326
|
+
- `@robota-sdk/agent-transport` — lean core: headless adapter + `TransportRegistry` + scripted-provider testing fixtures (no external runtime deps).
|
|
327
|
+
- `@robota-sdk/agent-transport-tui` — React + Ink terminal UI.
|
|
328
|
+
- `@robota-sdk/agent-transport-ws` — WebSocket transport + protocol (`agent-web-ui` now depends only on this for WS types).
|
|
329
|
+
- `@robota-sdk/agent-transport-http` — Hono HTTP transport.
|
|
330
|
+
- `@robota-sdk/agent-transport-mcp` — MCP server transport.
|
|
331
|
+
|
|
332
|
+
The default transport-registry wiring (pre-registering `WsTransport`) moves to the CLI composition root, removing the core→ws edge.
|
|
333
|
+
|
|
334
|
+
### Patch Changes
|
|
335
|
+
|
|
336
|
+
- Updated dependencies
|
|
337
|
+
- Updated dependencies [c0a6287]
|
|
338
|
+
- Updated dependencies [9df3a88]
|
|
339
|
+
- Updated dependencies
|
|
340
|
+
- Updated dependencies
|
|
341
|
+
- Updated dependencies [576af62]
|
|
342
|
+
- @robota-sdk/agent-core@3.0.0-beta.76
|
|
343
|
+
- @robota-sdk/agent-framework@3.0.0-beta.76
|
|
344
|
+
- @robota-sdk/agent-interface-transport@3.0.0-beta.76
|
package/dist/node/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
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}:{},...t.sessionDirectory?{sessionDirectory:t.sessionDirectory}:{}});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),...e.sessionDirectory?{sessionDirectory:e.sessionDirectory}:{}}}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;sessionBinder;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),this.sessionBinder=e.sessionBinder}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)}bindConnection(e){if(!this.sessionBinder)return{binding:void 0};try{return{binding:this.sessionBinder.bind(`drive`)}}catch{return e.close(1013,`session unavailable`),null}}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=this.bindConnection(t);if(i===null)return;let{binding:a}=i,o=a?.session??r,s=new h(t),l=(0,e.createSessionMessageHandler)({session:o,deliver:s.deliver,...this.handlerOptions,...a?{sessionDirectory:a.directory}:{}});s.bindProtocolCleanup(()=>{l.cleanup(),a?.release()}),s.bindSinkDetach(_.addSink(e=>s.deliverBinary(e))),t.on(`message`,(e,t)=>{if(!t){l.onMessage(String(e));return}let n=_.receive(m(e));n.ok||s.deliver({type:`protocol_error`,message:n.error})}),t.on(`close`,s.close),t.on(`error`,s.close),s.deliver({type:`messages`,messages:o.getMessages()}),s.deliver({type:`execution_workspace_event`,snapshot:o.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;
|
package/dist/node/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TUsageSurface } from "@robota-sdk/agent-interface-analytics";
|
|
2
|
-
import { TDriverId } from "@robota-sdk/agent-interface-session";
|
|
2
|
+
import { ISessionBinder, TDriverId } from "@robota-sdk/agent-interface-session";
|
|
3
3
|
import { IChannelDescriptor, IConfigurableTransport, IPayloadChannel, IPayloadChannelHost, ITransportAdapter, TChannelEventMap, TChannelReceiveResult } from "@robota-sdk/agent-interface-transport";
|
|
4
4
|
import { IProtocolSession, ISessionMessageHandlerOptions, TServerMessage } from "@robota-sdk/agent-transport";
|
|
5
5
|
import { TUniversalValue } from "@robota-sdk/agent-core";
|
|
@@ -12,6 +12,7 @@ interface IWsTransportOptions {
|
|
|
12
12
|
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
13
13
|
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
14
14
|
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
15
|
+
sessionDirectory?: ISessionMessageHandlerOptions['sessionDirectory'];
|
|
15
16
|
driverId?: TDriverId;
|
|
16
17
|
surface?: TUsageSurface;
|
|
17
18
|
}
|
|
@@ -32,6 +33,15 @@ interface IWsTransportConfig {
|
|
|
32
33
|
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
33
34
|
/** Host-owned stored-session trace/cost producer for personal-usage drill-down. */
|
|
34
35
|
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
36
|
+
/** Host-owned session directory (#3189): lets every connection list, start and switch sessions. */
|
|
37
|
+
sessionDirectory?: ISessionMessageHandlerOptions['sessionDirectory'];
|
|
38
|
+
/**
|
|
39
|
+
* #3189: gives each connection a binding of its own — its own session and its own directory view —
|
|
40
|
+
* so one connection's switch moves only that connection. Released when the connection closes. When
|
|
41
|
+
* set, a connection uses its binding's session and directory instead of the attached session and
|
|
42
|
+
* `sessionDirectory`; without it every connection shares the attached session.
|
|
43
|
+
*/
|
|
44
|
+
sessionBinder?: ISessionBinder<IProtocolSession>;
|
|
35
45
|
port?: number;
|
|
36
46
|
maxRetries?: number;
|
|
37
47
|
/**
|
|
@@ -105,6 +115,7 @@ declare class WsTransport implements IConfigurableTransport<IProtocolSession>, I
|
|
|
105
115
|
private readonly allowedHosts;
|
|
106
116
|
private readonly allowedOrigins;
|
|
107
117
|
private readonly handlerOptions;
|
|
118
|
+
private readonly sessionBinder;
|
|
108
119
|
private readonly channels;
|
|
109
120
|
private resolvedPort?;
|
|
110
121
|
constructor(config?: IWsTransportConfig);
|
|
@@ -129,6 +140,11 @@ declare class WsTransport implements IConfigurableTransport<IProtocolSession>, I
|
|
|
129
140
|
/** TRANS-002 (issue #2480): persisted `port`/`maxRetries` reach the server through here, before start. */
|
|
130
141
|
configure(options: Record<string, TUniversalValue>): void;
|
|
131
142
|
validateOptions(options: Record<string, TUniversalValue>): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* The connection's binding, `{ binding: undefined }` without a binder, or `null` when the host
|
|
145
|
+
* would not bind it (shutting down, say): the socket is then closed before any session data.
|
|
146
|
+
*/
|
|
147
|
+
private bindConnection;
|
|
132
148
|
private bindWithRetry;
|
|
133
149
|
private tryBind;
|
|
134
150
|
}
|
|
@@ -1 +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;;;
|
|
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,mBAAmB;EACnB,WAAW;EACX,UAAU;;UAGK,qBAAqB,kBAAkB;EACtD,aAAa;;iBAGC,kBAAkB,SAAS,sBAAsB;;;UCrBhD;;EAEf,WAAW;;EAEX,UAAU;;EAEV,wBAAwB;;EAExB,gBAAgB;;EAEhB,6BAA6B;;EAE7B,mBAAmB;;;;;;;EAOnB,gBAAgB,eAAe;EAC/B;EACA;;;;;;;;;;;EAWA;;;;;;;;;;EAUA;;EAEA;;;;;EAKA;;;;;;EAMA;;;;;;;;;cClBW,uBAAuB,uBAAuB,mBAAmB;WACnE;WACA,WAAS;;;WACT;WACA;;;;;;;;;;;;UASD;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;mBACA;UACT;EAEI,YAAA,SAAQ;EAapB,OAAO,SAAS;;;;;;;EAWhB,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;MAKf;;;;;;MASA;EAIE,SAAS;EA8BT,QAAQ;;EAmBd,UAAU,SAAS,eAAe;EAWlC,gBAAgB,SAAS,eAAe;;;;;UAQhC;UAYA;UAYA;;;;;KC3LE,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,6 +1,6 @@
|
|
|
1
1
|
import { IProtocolSession, ISessionMessageHandlerOptions, TServerMessage } from "@robota-sdk/agent-transport";
|
|
2
2
|
import { TUsageSurface } from "@robota-sdk/agent-interface-analytics";
|
|
3
|
-
import { TDriverId } from "@robota-sdk/agent-interface-session";
|
|
3
|
+
import { ISessionBinder, TDriverId } from "@robota-sdk/agent-interface-session";
|
|
4
4
|
import { IChannelDescriptor, IConfigurableTransport, IPayloadChannel, IPayloadChannelHost, ITransportAdapter, TChannelEventMap, TChannelReceiveResult } from "@robota-sdk/agent-interface-transport";
|
|
5
5
|
import { TUniversalValue } from "@robota-sdk/agent-core";
|
|
6
6
|
//#region src/ws-transport.d.ts
|
|
@@ -12,6 +12,7 @@ interface IWsTransportOptions {
|
|
|
12
12
|
personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
|
|
13
13
|
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
14
14
|
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
15
|
+
sessionDirectory?: ISessionMessageHandlerOptions['sessionDirectory'];
|
|
15
16
|
driverId?: TDriverId;
|
|
16
17
|
surface?: TUsageSurface;
|
|
17
18
|
}
|
|
@@ -32,6 +33,15 @@ interface IWsTransportConfig {
|
|
|
32
33
|
usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
|
|
33
34
|
/** Host-owned stored-session trace/cost producer for personal-usage drill-down. */
|
|
34
35
|
storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
|
|
36
|
+
/** Host-owned session directory (#3189): lets every connection list, start and switch sessions. */
|
|
37
|
+
sessionDirectory?: ISessionMessageHandlerOptions['sessionDirectory'];
|
|
38
|
+
/**
|
|
39
|
+
* #3189: gives each connection a binding of its own — its own session and its own directory view —
|
|
40
|
+
* so one connection's switch moves only that connection. Released when the connection closes. When
|
|
41
|
+
* set, a connection uses its binding's session and directory instead of the attached session and
|
|
42
|
+
* `sessionDirectory`; without it every connection shares the attached session.
|
|
43
|
+
*/
|
|
44
|
+
sessionBinder?: ISessionBinder<IProtocolSession>;
|
|
35
45
|
port?: number;
|
|
36
46
|
maxRetries?: number;
|
|
37
47
|
/**
|
|
@@ -105,6 +115,7 @@ declare class WsTransport implements IConfigurableTransport<IProtocolSession>, I
|
|
|
105
115
|
private readonly allowedHosts;
|
|
106
116
|
private readonly allowedOrigins;
|
|
107
117
|
private readonly handlerOptions;
|
|
118
|
+
private readonly sessionBinder;
|
|
108
119
|
private readonly channels;
|
|
109
120
|
private resolvedPort?;
|
|
110
121
|
constructor(config?: IWsTransportConfig);
|
|
@@ -129,6 +140,11 @@ declare class WsTransport implements IConfigurableTransport<IProtocolSession>, I
|
|
|
129
140
|
/** TRANS-002 (issue #2480): persisted `port`/`maxRetries` reach the server through here, before start. */
|
|
130
141
|
configure(options: Record<string, TUniversalValue>): void;
|
|
131
142
|
validateOptions(options: Record<string, TUniversalValue>): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* The connection's binding, `{ binding: undefined }` without a binder, or `null` when the host
|
|
145
|
+
* would not bind it (shutting down, say): the socket is then closed before any session data.
|
|
146
|
+
*/
|
|
147
|
+
private bindConnection;
|
|
132
148
|
private bindWithRetry;
|
|
133
149
|
private tryBind;
|
|
134
150
|
}
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;;;
|
|
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,mBAAmB;EACnB,WAAW;EACX,UAAU;;UAGK,qBAAqB,kBAAkB;EACtD,aAAa;;iBAGC,kBAAkB,SAAS,sBAAsB;;;UCrBhD;;EAEf,WAAW;;EAEX,UAAU;;EAEV,wBAAwB;;EAExB,gBAAgB;;EAEhB,6BAA6B;;EAE7B,mBAAmB;;;;;;;EAOnB,gBAAgB,eAAe;EAC/B;EACA;;;;;;;;;;;EAWA;;;;;;;;;;EAUA;;EAEA;;;;;EAKA;;;;;;EAMA;;;;;;;;;cClBW,uBAAuB,uBAAuB,mBAAmB;WACnE;WACA,WAAS;;;WACT;WACA;;;;;;;;;;;;UASD;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;mBACA;UACT;EAEI,YAAA,SAAQ;EAapB,OAAO,SAAS;;;;;;;EAWhB,gBAAgB,gBAAgB,kBAC9B,YAAY,mBAAmB,WAC9B,gBAAgB;;MAKf;;;;;;MASA;EAIE,SAAS;EA8BT,QAAQ;;EAmBd,UAAU,SAAS,eAAe;EAWlC,gBAAgB,SAAS,eAAe;;;;;UAQhC;UAYA;UAYA;;;;;KC3LE,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.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{DEFAULT_MAX_PENDING_BYTES as e,createOutboundDelivery as t,createPendingStallClock as n,createSessionMessageHandler as r,decodeChannelFrame as i,encodeBinaryFrame as a,encodeChannelEventFrame as o,isOverPendingBudget as s}from"@robota-sdk/agent-transport";import{createServer as c}from"node:http";import{WebSocket as l,WebSocketServer as u}from"ws";import{timingSafeEqual as d}from"node:crypto";import{resolveAdmission as f}from"@robota-sdk/agent-transport/node";function p(e){let n=null,i=null,a=e=>Object.assign(Error(`WebSocket transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`ws`}),o={name:`ws`,lifecycle:Object.freeze({kind:`service`}),onMessage:null,attach(e){n=e},async start(){if(!n)throw a(`not-attached`);if(i)throw a(`already-started`);let s=t(e.send,(t,n)=>{c.cleanup(),i=null,o.onMessage=null,e.onDeliveryError?.(t,n)}),c=r({session:n,deliver:s,...e.driverId?{driverId:e.driverId}:{},...e.surface?{surface:e.surface}:{},...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{}});i=c.cleanup,this.onMessage=c.onMessage},async stop(){i?.(),i=null,this.onMessage=null,n=null}};return o}var m=class e{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(e){let t=i(e);if(!t.ok)return t;let{frame:n}=t,r=this.channels.get(n.channel);if(!r||!r.open)return{ok:!1,error:`No channel registered for "${n.channel}"`};if(n.kind===`binary`){if(r.descriptor.binary!==!0)return{ok:!1,error:`Channel "${n.channel}" did not declare binary payload support`};for(let e of[...r.binaryHandlers])e(n);return{ok:!0,frame:n}}if(!r.declaredEvents.has(n.event))return{ok:!1,error:`Channel "${n.channel}" did not declare event "${n.event}"`};let a=r.eventHandlers.get(n.event);if(a)for(let e of[...a])e(n.payload,n);return{ok:!0,frame:n}}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(t,n){return{descriptor:t,sendEvent:(r,i)=>{if(e.assertOpen(n),!n.declaredEvents.has(r))throw Error(`Channel "${t.name}" did not declare event "${r}"`);let a=n.seq;n.seq+=1,this.broadcast(o({kind:`event`,channel:t.name,seq:a,event:r,payload:i}))},sendBinary:r=>{if(e.assertOpen(n),t.binary!==!0)throw Error(`Channel "${t.name}" did not declare binary payload support`);let i=n.seq;n.seq+=1,this.broadcast(a({kind:`binary`,channel:t.name,seq:i,payload:r}))},onEvent:(r,i)=>{if(e.assertOpen(n),!n.declaredEvents.has(r))throw Error(`Channel "${t.name}" did not declare event "${r}"`);let a=n.eventHandlers.get(r)??new Set,o=i;return a.add(o),n.eventHandlers.set(r,a),()=>{a.delete(o)}},onBinary:t=>(e.assertOpen(n),n.binaryHandlers.add(t),()=>{n.binaryHandlers.delete(t)}),close:()=>{n.open=!1,n.eventHandlers.clear(),n.binaryHandlers.clear(),this.channels.delete(t.name)}}}};const h=new Set([`localhost`,`127.0.0.1`,`::1`,`[::1]`]);function g(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 _(e,t){let n=g(e.headers.host);return n===null?!1:h.has(n)||t.has(n)}function v(e,t){let n=e.headers.origin;if(n===void 0||t.has(n))return!0;try{return h.has(new URL(n).hostname)}catch{return!1}}function y(e,t){if(!t)return!1;let n=Buffer.from(e),r=Buffer.from(t);return n.length===r.length&&d(n,r)}function b(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 x(e){return f({...e.token===void 0?{}:{token:e.token},...e.open===!0?{open:!0}:{},...e.openReason===void 0?{}:{openReason:e.openReason}})}function S(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 C=class{socket;cleanupProtocol=()=>void 0;detachSink=()=>void 0;closed=!1;stallClock=n();deliver;constructor(n){this.socket=n,this.deliver=t(e=>this.rawSend(e),()=>this.close(),()=>this.socket.bufferedAmount,e,this.stallClock)}deliverBinary=e=>{if(!this.closed&&this.socket.readyState===l.OPEN){if(s(this.socket.bufferedAmount)||this.stallClock.observe(this.socket.bufferedAmount)!==void 0){this.close();return}this.socket.send(e,{binary:!0},e=>{e&&this.close()})}};rawSend(e){if(this.socket.readyState!==l.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===l.OPEN||this.socket.readyState===l.CONNECTING)&&this.socket.close(1011,`session event delivery failed`))}};function w(e){return{...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{}}}function T(e){return{...e.driverId?{driverId:e.driverId}:{},...e.surface?{surface:e.surface}:{},...w(e)}}function E(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 D(e,t){return Object.assign(Error(`${e} ${t}.`),{name:`TransportLifecycleError`,code:t,transportName:e})}var O=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 m;resolvedPort;constructor(e={}){this.port=e.port??7070,this.maxRetries=e.maxRetries??20;let t=x(e);t.token!==null&&(this.token=t.token),this.allowedHosts=new Set(e.allowedHosts??[]),this.allowedOrigins=new Set(e.allowedOrigins??[]),this.handlerOptions=T(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 D(this.name,`not-attached`);if(this.state===`starting`||this.state===`ready`||this.state===`stopping`)throw D(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 D(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 E(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(e,t){return new Promise((n,i)=>{let a=c((e,t)=>{t.writeHead(400).end(`WebSocket endpoint`)});a.on(`error`,e=>{a.close(),i(e)});let o=this.token,s=this.allowedHosts,l=this.allowedOrigins,d=this.channels;a.listen(t,`127.0.0.1`,()=>{let i=new u({server:a,verifyClient:(e,t)=>{if(!_(e.req,s)){t(!1,403,`Forbidden host`);return}if(!v(e.req,l)){t(!1,403,`Forbidden origin`);return}t(!0)}});i.on(`connection`,(t,n)=>{if(o!==void 0&&!y(o,b(n))){t.close(1008,`unauthorized`);return}let i=new C(t),
|
|
1
|
+
import{DEFAULT_MAX_PENDING_BYTES as e,createOutboundDelivery as t,createPendingStallClock as n,createSessionMessageHandler as r,decodeChannelFrame as i,encodeBinaryFrame as a,encodeChannelEventFrame as o,isOverPendingBudget as s}from"@robota-sdk/agent-transport";import{createServer as c}from"node:http";import{WebSocket as l,WebSocketServer as u}from"ws";import{timingSafeEqual as d}from"node:crypto";import{resolveAdmission as f}from"@robota-sdk/agent-transport/node";function p(e){let n=null,i=null,a=e=>Object.assign(Error(`WebSocket transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`ws`}),o={name:`ws`,lifecycle:Object.freeze({kind:`service`}),onMessage:null,attach(e){n=e},async start(){if(!n)throw a(`not-attached`);if(i)throw a(`already-started`);let s=t(e.send,(t,n)=>{c.cleanup(),i=null,o.onMessage=null,e.onDeliveryError?.(t,n)}),c=r({session:n,deliver:s,...e.driverId?{driverId:e.driverId}:{},...e.surface?{surface:e.surface}:{},...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{},...e.sessionDirectory?{sessionDirectory:e.sessionDirectory}:{}});i=c.cleanup,this.onMessage=c.onMessage},async stop(){i?.(),i=null,this.onMessage=null,n=null}};return o}var m=class e{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(e){let t=i(e);if(!t.ok)return t;let{frame:n}=t,r=this.channels.get(n.channel);if(!r||!r.open)return{ok:!1,error:`No channel registered for "${n.channel}"`};if(n.kind===`binary`){if(r.descriptor.binary!==!0)return{ok:!1,error:`Channel "${n.channel}" did not declare binary payload support`};for(let e of[...r.binaryHandlers])e(n);return{ok:!0,frame:n}}if(!r.declaredEvents.has(n.event))return{ok:!1,error:`Channel "${n.channel}" did not declare event "${n.event}"`};let a=r.eventHandlers.get(n.event);if(a)for(let e of[...a])e(n.payload,n);return{ok:!0,frame:n}}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(t,n){return{descriptor:t,sendEvent:(r,i)=>{if(e.assertOpen(n),!n.declaredEvents.has(r))throw Error(`Channel "${t.name}" did not declare event "${r}"`);let a=n.seq;n.seq+=1,this.broadcast(o({kind:`event`,channel:t.name,seq:a,event:r,payload:i}))},sendBinary:r=>{if(e.assertOpen(n),t.binary!==!0)throw Error(`Channel "${t.name}" did not declare binary payload support`);let i=n.seq;n.seq+=1,this.broadcast(a({kind:`binary`,channel:t.name,seq:i,payload:r}))},onEvent:(r,i)=>{if(e.assertOpen(n),!n.declaredEvents.has(r))throw Error(`Channel "${t.name}" did not declare event "${r}"`);let a=n.eventHandlers.get(r)??new Set,o=i;return a.add(o),n.eventHandlers.set(r,a),()=>{a.delete(o)}},onBinary:t=>(e.assertOpen(n),n.binaryHandlers.add(t),()=>{n.binaryHandlers.delete(t)}),close:()=>{n.open=!1,n.eventHandlers.clear(),n.binaryHandlers.clear(),this.channels.delete(t.name)}}}};const h=new Set([`localhost`,`127.0.0.1`,`::1`,`[::1]`]);function g(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 _(e,t){let n=g(e.headers.host);return n===null?!1:h.has(n)||t.has(n)}function v(e,t){let n=e.headers.origin;if(n===void 0||t.has(n))return!0;try{return h.has(new URL(n).hostname)}catch{return!1}}function y(e,t){if(!t)return!1;let n=Buffer.from(e),r=Buffer.from(t);return n.length===r.length&&d(n,r)}function b(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 x(e){return f({...e.token===void 0?{}:{token:e.token},...e.open===!0?{open:!0}:{},...e.openReason===void 0?{}:{openReason:e.openReason}})}function S(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 C=class{socket;cleanupProtocol=()=>void 0;detachSink=()=>void 0;closed=!1;stallClock=n();deliver;constructor(n){this.socket=n,this.deliver=t(e=>this.rawSend(e),()=>this.close(),()=>this.socket.bufferedAmount,e,this.stallClock)}deliverBinary=e=>{if(!this.closed&&this.socket.readyState===l.OPEN){if(s(this.socket.bufferedAmount)||this.stallClock.observe(this.socket.bufferedAmount)!==void 0){this.close();return}this.socket.send(e,{binary:!0},e=>{e&&this.close()})}};rawSend(e){if(this.socket.readyState!==l.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===l.OPEN||this.socket.readyState===l.CONNECTING)&&this.socket.close(1011,`session event delivery failed`))}};function w(e){return{...e.personalUsageReporter?{personalUsageReporter:e.personalUsageReporter}:{},...e.usageReporter?{usageReporter:e.usageReporter}:{},...e.storedSessionUsageReporter?{storedSessionUsageReporter:e.storedSessionUsageReporter}:{}}}function T(e){return{...e.driverId?{driverId:e.driverId}:{},...e.surface?{surface:e.surface}:{},...w(e),...e.sessionDirectory?{sessionDirectory:e.sessionDirectory}:{}}}function E(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 D(e,t){return Object.assign(Error(`${e} ${t}.`),{name:`TransportLifecycleError`,code:t,transportName:e})}var O=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;sessionBinder;channels=new m;resolvedPort;constructor(e={}){this.port=e.port??7070,this.maxRetries=e.maxRetries??20;let t=x(e);t.token!==null&&(this.token=t.token),this.allowedHosts=new Set(e.allowedHosts??[]),this.allowedOrigins=new Set(e.allowedOrigins??[]),this.handlerOptions=T(e),this.sessionBinder=e.sessionBinder}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 D(this.name,`not-attached`);if(this.state===`starting`||this.state===`ready`||this.state===`stopping`)throw D(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 D(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 E(e)}bindConnection(e){if(!this.sessionBinder)return{binding:void 0};try{return{binding:this.sessionBinder.bind(`drive`)}}catch{return e.close(1013,`session unavailable`),null}}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(e,t){return new Promise((n,i)=>{let a=c((e,t)=>{t.writeHead(400).end(`WebSocket endpoint`)});a.on(`error`,e=>{a.close(),i(e)});let o=this.token,s=this.allowedHosts,l=this.allowedOrigins,d=this.channels;a.listen(t,`127.0.0.1`,()=>{let i=new u({server:a,verifyClient:(e,t)=>{if(!_(e.req,s)){t(!1,403,`Forbidden host`);return}if(!v(e.req,l)){t(!1,403,`Forbidden origin`);return}t(!0)}});i.on(`connection`,(t,n)=>{if(o!==void 0&&!y(o,b(n))){t.close(1008,`unauthorized`);return}let i=this.bindConnection(t);if(i===null)return;let{binding:a}=i,s=a?.session??e,c=new C(t),l=r({session:s,deliver:c.deliver,...this.handlerOptions,...a?{sessionDirectory:a.directory}:{}});c.bindProtocolCleanup(()=>{l.cleanup(),a?.release()}),c.bindSinkDetach(d.addSink(e=>c.deliverBinary(e))),t.on(`message`,(e,t)=>{if(!t){l.onMessage(String(e));return}let n=d.receive(S(e));n.ok||c.deliver({type:`protocol_error`,message:n.error})}),t.on(`close`,c.close),t.on(`error`,c.close),c.deliver({type:`messages`,messages:s.getMessages()}),c.deliver({type:`execution_workspace_event`,snapshot:s.getExecutionWorkspaceSnapshot()})}),n({port:t,stop:()=>new Promise(e=>{for(let e of i.clients)e.close(1001,`server shutting down`);let t=setTimeout(()=>{for(let e of i.clients)e.terminate()},5e3);i.close(()=>{clearTimeout(t),a.close(()=>e())})})})})})}};export{m as PayloadChannelRegistry,O as WsTransport,p as createWsTransport};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|