@johnhenry/wsh 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.d.ts CHANGED
@@ -126,6 +126,7 @@ export const MSG: {
126
126
  readonly REVERSE_CONNECT: 0x53;
127
127
  readonly REVERSE_ACCEPT: 0x54;
128
128
  readonly REVERSE_REJECT: 0x55;
129
+ readonly RELAY_FORWARD: 0x56;
129
130
 
130
131
  // Gateway
131
132
  readonly OPEN_TCP: 0x70;
@@ -249,6 +250,7 @@ export function serverHello(opts?: {
249
250
 
250
251
  export function challenge(opts?: {
251
252
  nonce?: Uint8Array;
253
+ sessionId?: string;
252
254
  }): WshMessage;
253
255
 
254
256
  export function authMethods(opts?: {
@@ -264,7 +266,7 @@ export function auth(opts?: {
264
266
 
265
267
  export function authOk(opts?: {
266
268
  sessionId?: string;
267
- token?: string;
269
+ token?: Uint8Array;
268
270
  ttl?: number;
269
271
  }): WshMessage;
270
272
 
@@ -287,6 +289,8 @@ export function openOk(opts?: {
287
289
  streamIds?: number[];
288
290
  dataMode?: string;
289
291
  capabilities?: string[];
292
+ sessionId?: string;
293
+ token?: Uint8Array;
290
294
  }): WshMessage;
291
295
 
292
296
  export function openFail(opts?: {
@@ -337,14 +341,14 @@ export function pong(opts?: {
337
341
 
338
342
  export function attach(opts?: {
339
343
  sessionId?: string;
340
- token?: string;
344
+ token?: Uint8Array;
341
345
  mode?: string;
342
346
  deviceLabel?: string;
343
347
  }): WshMessage;
344
348
 
345
349
  export function resume(opts?: {
346
350
  sessionId?: string;
347
- token?: string;
351
+ token?: Uint8Array;
348
352
  lastSeq?: number;
349
353
  }): WshMessage;
350
354
 
@@ -410,6 +414,10 @@ export function reverseRegister(opts?: {
410
414
  supportsEcho?: boolean;
411
415
  supportsTermSync?: boolean;
412
416
  publicKey?: Uint8Array | null;
417
+ /** The signing peer's own monotonic counter for its signed peer record. */
418
+ seq?: number;
419
+ /** Ed25519 signature over the peer record transcript (see signPeerRecord). */
420
+ recordSignature?: Uint8Array;
413
421
  }): WshMessage;
414
422
 
415
423
  export function reverseList(): WshMessage;
@@ -536,6 +544,262 @@ export function gatewayData(opts?: {
536
544
  data?: Uint8Array;
537
545
  }): WshMessage;
538
546
 
547
+ // -- Message constructors (session extras) --
548
+
549
+ export function clipboard(opts?: {
550
+ direction?: string;
551
+ data?: string;
552
+ }): WshMessage;
553
+
554
+ export function recordingExport(opts?: {
555
+ sessionId?: string;
556
+ format?: string;
557
+ data?: string;
558
+ }): WshMessage;
559
+
560
+ export function commandJournal(opts?: {
561
+ sessionId?: string;
562
+ command?: string;
563
+ exitCode?: number;
564
+ durationMs?: number;
565
+ cwd?: string;
566
+ timestamp?: number;
567
+ }): WshMessage;
568
+
569
+ export function metricsRequest(): WshMessage;
570
+
571
+ export function suspendSession(opts?: {
572
+ sessionId?: string;
573
+ action?: string;
574
+ }): WshMessage;
575
+
576
+ export function restartPty(opts?: {
577
+ sessionId?: string;
578
+ command?: string;
579
+ }): WshMessage;
580
+
581
+ export function sessionListRequest(): WshMessage;
582
+
583
+ export function sessionList(opts?: {
584
+ sessions?: unknown[];
585
+ }): WshMessage;
586
+
587
+ export function detach(opts?: {
588
+ sessionId?: string;
589
+ }): WshMessage;
590
+
591
+ export function detachOk(opts?: {
592
+ sessionId?: string;
593
+ }): WshMessage;
594
+
595
+ export function detachFail(opts?: {
596
+ reason?: string;
597
+ }): WshMessage;
598
+
599
+ // -- Message constructors (guest sharing) --
600
+
601
+ export function guestInvite(opts?: {
602
+ sessionId?: string;
603
+ ttl?: number;
604
+ permissions?: string[];
605
+ }): WshMessage;
606
+
607
+ export function guestJoin(opts?: {
608
+ token?: string;
609
+ deviceLabel?: string;
610
+ }): WshMessage;
611
+
612
+ export function guestRevoke(opts?: {
613
+ token?: string;
614
+ reason?: string;
615
+ }): WshMessage;
616
+
617
+ export function shareSession(opts?: {
618
+ sessionId?: string;
619
+ mode?: string;
620
+ ttl?: number;
621
+ }): WshMessage;
622
+
623
+ export function shareRevoke(opts?: {
624
+ shareId?: string;
625
+ reason?: string;
626
+ }): WshMessage;
627
+
628
+ // -- Message constructors (compression / rate control / linking) --
629
+
630
+ export function compressBegin(opts?: {
631
+ algorithm?: string;
632
+ level?: number;
633
+ }): WshMessage;
634
+
635
+ export function compressAck(opts?: {
636
+ algorithm?: string;
637
+ accepted?: boolean;
638
+ }): WshMessage;
639
+
640
+ export function rateControl(opts?: {
641
+ sessionId?: string;
642
+ maxBytesPerSec?: number;
643
+ policy?: string;
644
+ }): WshMessage;
645
+
646
+ export function rateWarning(opts?: {
647
+ sessionId?: string;
648
+ queuedBytes?: number;
649
+ action?: string;
650
+ }): WshMessage;
651
+
652
+ export function sessionLink(opts?: {
653
+ sourceSession?: string;
654
+ targetHost?: string;
655
+ targetPort?: number;
656
+ targetUser?: string;
657
+ }): WshMessage;
658
+
659
+ export function sessionUnlink(opts?: {
660
+ linkId?: string;
661
+ reason?: string;
662
+ }): WshMessage;
663
+
664
+ // -- Message constructors (copilot) --
665
+
666
+ export function copilotAttach(opts?: {
667
+ sessionId?: string;
668
+ model?: string;
669
+ contextWindow?: number;
670
+ }): WshMessage;
671
+
672
+ export function copilotSuggest(opts?: {
673
+ sessionId?: string;
674
+ suggestion?: string;
675
+ confidence?: number;
676
+ }): WshMessage;
677
+
678
+ export function copilotDetach(opts?: {
679
+ sessionId?: string;
680
+ reason?: string;
681
+ }): WshMessage;
682
+
683
+ // -- Message constructors (e2e / echo / termsync) --
684
+
685
+ export function keyExchange(opts?: {
686
+ algorithm?: string;
687
+ publicKey?: Uint8Array;
688
+ sessionId?: string;
689
+ kemPublicKey?: Uint8Array;
690
+ kemCiphertext?: Uint8Array;
691
+ }): WshMessage;
692
+
693
+ export function encryptedFrame(opts?: {
694
+ nonce?: Uint8Array;
695
+ ciphertext?: Uint8Array;
696
+ sessionId?: string;
697
+ }): WshMessage;
698
+
699
+ export function echoAck(opts?: {
700
+ channelId?: number;
701
+ echoSeq?: number;
702
+ }): WshMessage;
703
+
704
+ export function echoState(opts?: {
705
+ channelId?: number;
706
+ echoSeq?: number;
707
+ cursorX?: number;
708
+ cursorY?: number;
709
+ pending?: number;
710
+ }): WshMessage;
711
+
712
+ export function termSync(opts?: {
713
+ channelId?: number;
714
+ frameSeq?: number;
715
+ stateHash?: Uint8Array;
716
+ }): WshMessage;
717
+
718
+ export function termDiff(opts?: {
719
+ channelId?: number;
720
+ frameSeq?: number;
721
+ baseSeq?: number;
722
+ patch?: Uint8Array;
723
+ }): WshMessage;
724
+
725
+ // -- Message constructors (scaling / principals) --
726
+
727
+ export function nodeAnnounce(opts?: {
728
+ nodeId?: string;
729
+ endpoint?: string;
730
+ load?: number;
731
+ capacity?: number;
732
+ }): WshMessage;
733
+
734
+ export function nodeRedirect(opts?: {
735
+ targetNode?: string;
736
+ targetEndpoint?: string;
737
+ sessionId?: string;
738
+ reason?: string;
739
+ }): WshMessage;
740
+
741
+ export function sessionGrant(opts?: {
742
+ sessionId?: string;
743
+ principal?: string;
744
+ permissions?: string[];
745
+ }): WshMessage;
746
+
747
+ export function sessionRevoke(opts?: {
748
+ sessionId?: string;
749
+ principal?: string;
750
+ reason?: string;
751
+ }): WshMessage;
752
+
753
+ // -- Message constructors (file channel / policy / terminal) --
754
+
755
+ export function fileOp(opts?: {
756
+ channelId?: number;
757
+ op?: string;
758
+ path?: string;
759
+ offset?: number;
760
+ length?: number;
761
+ }): WshMessage;
762
+
763
+ export function fileResult(opts?: {
764
+ channelId?: number;
765
+ success?: boolean;
766
+ metadata?: Record<string, unknown>;
767
+ errorMessage?: string;
768
+ }): WshMessage;
769
+
770
+ export function fileChunk(opts?: {
771
+ channelId?: number;
772
+ offset?: number;
773
+ data?: Uint8Array;
774
+ isFinal?: boolean;
775
+ totalSize?: number;
776
+ }): WshMessage;
777
+
778
+ export function policyEval(opts?: {
779
+ requestId?: string;
780
+ action?: string;
781
+ principal?: string;
782
+ context?: Record<string, unknown>;
783
+ }): WshMessage;
784
+
785
+ export function policyResult(opts?: {
786
+ requestId?: string;
787
+ allowed?: boolean;
788
+ reason?: string;
789
+ }): WshMessage;
790
+
791
+ export function policyUpdate(opts?: {
792
+ policyId?: string;
793
+ rules?: Record<string, unknown>;
794
+ version?: number;
795
+ }): WshMessage;
796
+
797
+ export function terminalConfig(opts?: {
798
+ channelId?: number;
799
+ frontend?: string;
800
+ options?: Record<string, unknown>;
801
+ }): WshMessage;
802
+
539
803
  // -- Utility --
540
804
 
541
805
  /**
@@ -595,17 +859,30 @@ export function sign(privateKey: CryptoKey, data: Uint8Array): Promise<Uint8Arra
595
859
  */
596
860
  export function verify(publicKey: CryptoKey, signature: Uint8Array, data: Uint8Array): Promise<boolean>;
597
861
 
862
+ /** Options binding extra context into the auth transcript. */
863
+ export interface WshTranscriptOptions {
864
+ /** Username this transcript is bound to (default ''). */
865
+ username?: string;
866
+ /** Reserved for a transport-layer identity binding; no caller populates it yet. */
867
+ channelBinding?: Uint8Array;
868
+ }
869
+
598
870
  /**
599
871
  * Build the authentication transcript hash for challenge-response signing.
600
872
  *
601
- * transcript = SHA-256("wsh-v1\0" || session_id || nonce || channel_binding)
873
+ * transcript = SHA-256(
874
+ * "wsh-v1\0" || lp(username) || lp(session_id) || nonce || channel_binding
875
+ * )
876
+ *
877
+ * where lp() is a 4-byte big-endian length prefix on the variable-length
878
+ * string fields.
602
879
  *
603
880
  * @returns 32-byte SHA-256 hash
604
881
  */
605
882
  export function buildTranscript(
606
883
  sessionId: string,
607
884
  nonce: Uint8Array,
608
- channelBinding?: Uint8Array,
885
+ opts?: WshTranscriptOptions,
609
886
  ): Promise<Uint8Array>;
610
887
 
611
888
  /**
@@ -619,7 +896,7 @@ export function signChallenge(
619
896
  publicKey: CryptoKey,
620
897
  sessionId: string,
621
898
  nonce: Uint8Array,
622
- channelBinding?: Uint8Array,
899
+ opts?: WshTranscriptOptions,
623
900
  ): Promise<{ signature: Uint8Array; publicKeyRaw: Uint8Array }>;
624
901
 
625
902
  /**
@@ -630,7 +907,7 @@ export function verifyChallenge(
630
907
  signature: Uint8Array,
631
908
  sessionId: string,
632
909
  nonce: Uint8Array,
633
- channelBinding?: Uint8Array,
910
+ opts?: WshTranscriptOptions,
634
911
  ): Promise<boolean>;
635
912
 
636
913
  /** Fields of a signed peer record (reverse-mode registration). See `signPeerRecord`. */
@@ -789,18 +1066,194 @@ export class WshTransport {
789
1066
  */
790
1067
  export class WebTransportTransport extends WshTransport {}
791
1068
 
1069
+ /**
1070
+ * Dispatch a fixed, already-available batch of items one at a time,
1071
+ * fully awaiting each handler call before dispatching the next. Use
1072
+ * SerialQueue instead for items arriving incrementally via a push
1073
+ * callback.
1074
+ */
1075
+ export function dispatchSerially<T>(
1076
+ items: Iterable<T>,
1077
+ handler: (item: T) => void | Promise<void>,
1078
+ ): Promise<void>;
1079
+
1080
+ /**
1081
+ * A queue drained one item at a time, awaiting each dispatch before
1082
+ * starting the next. Use where items arrive incrementally over time via
1083
+ * a push callback (e.g. a transport's raw `message` event).
1084
+ */
1085
+ export class SerialQueue<T = unknown> {
1086
+ constructor(handler: (item: T) => void | Promise<void>);
1087
+ push(item: T): void;
1088
+ }
1089
+
792
1090
  // ============================================================================
793
1091
  // transport-ws.mjs -- WebSocket transport
794
1092
  // ============================================================================
795
1093
 
796
1094
  /**
797
- * wsh transport over a single WebSocket with multiplexed virtual streams.
1095
+ * wsh transport over a single WebSocket, multiplexed with QMux
1096
+ * (draft-ietf-quic-qmux-02). The control channel is QMux stream 0.
798
1097
  *
799
1098
  * Provides the same interface as WebTransportTransport so that upper
800
1099
  * layers (session, client) work identically over either transport.
801
1100
  */
802
1101
  export class WebSocketTransport extends WshTransport {}
803
1102
 
1103
+ /**
1104
+ * @deprecated The old hand-rolled 5-byte mux's frame-type byte values.
1105
+ * wsh now speaks QMux instead; this export is kept only for source
1106
+ * compatibility and is no longer produced or consumed anywhere.
1107
+ */
1108
+ export const WS_FRAME_TYPE: {
1109
+ readonly CONTROL: 0x01;
1110
+ readonly DATA: 0x02;
1111
+ readonly OPEN_STREAM: 0x03;
1112
+ readonly CLOSE_STREAM: 0x04;
1113
+ };
1114
+
1115
+ // ============================================================================
1116
+ // qmux.mjs / qmux-connection.mjs -- QMux multiplexing layer
1117
+ // ============================================================================
1118
+
1119
+ /** Default QMux flow-control parameters. */
1120
+ export const QMUX_DEFAULTS: {
1121
+ /** Connection-wide receive window in bytes (8 MiB). */
1122
+ readonly initialMaxData: number;
1123
+ /** Per-stream receive window in bytes (1 MiB). */
1124
+ readonly initialMaxStreamData: number;
1125
+ /** Initial bidirectional stream credit. */
1126
+ readonly initialMaxStreamsBidi: number;
1127
+ };
1128
+
1129
+ /** QUIC transport error codes used by wsh (RFC 9000 Section 20.1 subset). */
1130
+ export const QMUX_ERROR_CODE: {
1131
+ readonly NO_ERROR: 0x00;
1132
+ readonly INTERNAL_ERROR: 0x01;
1133
+ readonly FLOW_CONTROL_ERROR: 0x03;
1134
+ readonly STREAM_LIMIT_ERROR: 0x04;
1135
+ readonly STREAM_STATE_ERROR: 0x05;
1136
+ readonly FINAL_SIZE_ERROR: 0x06;
1137
+ readonly FRAME_ENCODING_ERROR: 0x07;
1138
+ readonly TRANSPORT_PARAMETER_ERROR: 0x08;
1139
+ readonly PROTOCOL_VIOLATION: 0x0a;
1140
+ readonly APPLICATION_ERROR: 0x0c;
1141
+ };
1142
+
1143
+ /** Stream-initiator bit values (QUIC stream-id low bit). */
1144
+ export const QMUX_STREAM_INITIATOR: {
1145
+ readonly CLIENT: 0;
1146
+ readonly SERVER: 1;
1147
+ };
1148
+
1149
+ /** First bidirectional stream ID for the given initiator (0 or 1). */
1150
+ export function firstBidiStreamId(initiator: 0 | 1): number;
1151
+
1152
+ /** Next bidirectional stream ID after `id` for the same initiator (+4). */
1153
+ export function nextBidiStreamId(id: number): number;
1154
+
1155
+ /** Whether a stream ID was opened by the client side. */
1156
+ export function isClientInitiated(streamId: number): boolean;
1157
+
1158
+ /** Whether a stream ID is bidirectional (wsh only uses bidi streams). */
1159
+ export function isBidirectional(streamId: number): boolean;
1160
+
1161
+ /**
1162
+ * One multiplexed QMux stream: a minimal push/pull data API (not DOM
1163
+ * Streams -- WebSocketTransport wraps these in ReadableStream/
1164
+ * WritableStream for the rest of wsh).
1165
+ */
1166
+ export interface QMuxStream {
1167
+ /** Stream ID. */
1168
+ id: number;
1169
+
1170
+ /** In-order data delivery. */
1171
+ onData: ((data: Uint8Array) => void) | null;
1172
+
1173
+ /** Clean FIN: all data delivered. */
1174
+ onEnd: (() => void) | null;
1175
+
1176
+ /** Peer aborted (after any reliable prefix was delivered). */
1177
+ onReset: ((errorCode: number) => void) | null;
1178
+
1179
+ /** The whole connection died (transport closed/errored). */
1180
+ onDestroy: ((err: Error) => void) | null;
1181
+
1182
+ /** Send state: 'ready' | 'send' | 'reset_sent' | 'data_sent'. */
1183
+ readonly sendState: string;
1184
+
1185
+ /** Receive state: 'recv' | 'size_known' | 'data_recvd' | 'reset_recvd'. */
1186
+ readonly recvState: string;
1187
+
1188
+ /** Write bytes, awaiting flow-control window as needed. */
1189
+ write(bytes: Uint8Array): Promise<void>;
1190
+
1191
+ /** Half-close the send side (send FIN). */
1192
+ close(): Promise<void>;
1193
+
1194
+ /**
1195
+ * Abort the send side. A non-zero `reliableSize` uses RESET_STREAM_AT
1196
+ * (draft-ietf-quic-reliable-stream-reset) to guarantee that prefix is
1197
+ * still delivered.
1198
+ */
1199
+ reset(errorCode?: number, reliableSize?: number): void;
1200
+
1201
+ /** Tell the peer to stop sending. */
1202
+ stopSending(errorCode?: number): void;
1203
+ }
1204
+
1205
+ /**
1206
+ * QMux connection: stream state machine + windowed flow control over an
1207
+ * ordered, reliable byte stream. Transport-agnostic -- it takes a
1208
+ * `send(bytes)` callback for outbound record bytes and is fed inbound
1209
+ * bytes via `receiveBytes()`. Exported so alternate server
1210
+ * implementations can speak the same framing as WebSocketTransport.
1211
+ */
1212
+ export class QMuxConnection {
1213
+ constructor(opts: {
1214
+ /** Whether this endpoint is the client (determines stream-id parity). */
1215
+ isClient: boolean;
1216
+ /** Callback receiving encoded QMux record bytes to transmit. */
1217
+ send: (bytes: Uint8Array) => void;
1218
+ initialMaxData?: number;
1219
+ initialMaxStreamData?: number;
1220
+ initialMaxStreamsBidi?: number;
1221
+ });
1222
+
1223
+ /** Called for peer-initiated streams. */
1224
+ onStreamOpen: ((stream: QMuxStream) => void) | null;
1225
+
1226
+ /** Called for DATAGRAM frames. */
1227
+ onDatagram: ((data: Uint8Array) => void) | null;
1228
+
1229
+ /** Called on peer CONNECTION_CLOSE. */
1230
+ onClose: ((errorCode: number, reason: string) => void) | null;
1231
+
1232
+ /** Called on protocol errors. */
1233
+ onError: ((err: Error) => void) | null;
1234
+
1235
+ /** Send the QX_TRANSPORT_PARAMETERS handshake frame -- must be the first frame sent. */
1236
+ sendHandshake(): void;
1237
+
1238
+ /** Open a new locally-initiated bidirectional stream. */
1239
+ openStream(): Promise<QMuxStream>;
1240
+
1241
+ /** Look up an existing stream by ID. */
1242
+ getStream(id: number): QMuxStream | undefined;
1243
+
1244
+ /** Send a DATAGRAM frame. */
1245
+ sendDatagram(bytes: Uint8Array): void;
1246
+
1247
+ /** Feed inbound bytes from the underlying transport. */
1248
+ receiveBytes(chunk: Uint8Array): void;
1249
+
1250
+ /** Send CONNECTION_CLOSE and shut down. */
1251
+ close(errorCode?: number, reason?: string): void;
1252
+
1253
+ /** Forcibly tear down every stream (transport died; nothing is sent). */
1254
+ destroy(err: Error): void;
1255
+ }
1256
+
804
1257
  // ============================================================================
805
1258
  // session.mjs -- WshSession
806
1259
  // ============================================================================
@@ -881,6 +1334,9 @@ export class WshSession {
881
1334
  /** Last terminal diff received for the session. */
882
1335
  readonly lastTermDiff: Record<string, unknown> | null;
883
1336
 
1337
+ /** Whether `enableE2E()` has been called and E2E sealing is active for this session. */
1338
+ readonly e2eEnabled: boolean;
1339
+
884
1340
  constructor(
885
1341
  transport: WshTransport,
886
1342
  channelId: number,
@@ -897,9 +1353,21 @@ export class WshSession {
897
1353
  /**
898
1354
  * Write data to the session's stdin stream.
899
1355
  * Accepts a Uint8Array for raw bytes or a string (UTF-8 encoded).
1356
+ * Sealed into an EncryptedFrame instead of plaintext SessionData when
1357
+ * `enableE2E()` has been called (virtual-mode sessions only).
900
1358
  */
901
1359
  write(data: Uint8Array | string): Promise<void>;
902
1360
 
1361
+ /**
1362
+ * Opt in to end-to-end encryption for this session's data plane
1363
+ * (virtual-mode sessions only). `sharedSecret` must come from a fresh
1364
+ * `WshClient.initiateE2E()` call on the *current* connection --
1365
+ * E2E state is connection-scoped, not session-scoped, so a session
1366
+ * Resumed/Attached on a new connection must run `initiateE2E()` and
1367
+ * `enableE2E()` again rather than reusing a prior key.
1368
+ */
1369
+ enableE2E(sharedSecret: CryptoKey, opts: { role: 'initiator' | 'responder' }): void;
1370
+
903
1371
  /**
904
1372
  * Request the remote PTY to resize.
905
1373
  */
@@ -1257,7 +1725,13 @@ export class WshClient {
1257
1725
  /**
1258
1726
  * Download a file from a remote path.
1259
1727
  */
1260
- download(remotePath: string): Promise<Uint8Array>;
1728
+ download(
1729
+ remotePath: string,
1730
+ opts?: {
1731
+ onProgress?: (progress: { received: number; total: number }) => void;
1732
+ timeout?: number;
1733
+ },
1734
+ ): Promise<Uint8Array>;
1261
1735
 
1262
1736
  /**
1263
1737
  * Discover MCP tools available on the remote server.
@@ -777,9 +777,10 @@ export function keyExchange({ algorithm, publicKey, sessionId, kemPublicKey, kem
777
777
  return msg;
778
778
  }
779
779
 
780
- export function encryptedFrame({ nonce, ciphertext, sessionId } = {}) {
780
+ export function encryptedFrame({ channelId, nonce, ciphertext, sessionId } = {}) {
781
781
  return {
782
782
  type: MSG.ENCRYPTED_FRAME,
783
+ channel_id: channelId,
783
784
  nonce,
784
785
  ciphertext,
785
786
  session_id: sessionId,