@johnhenry/wsh 0.13.0 → 0.15.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/README.md +67 -9
- package/package.json +2 -2
- package/src/client.mjs +86 -43
- package/src/e2e-frame.mjs +177 -0
- package/src/index.d.ts +529 -11
- package/src/messages.gen.mjs +9 -10
- package/src/session.mjs +164 -1
- package/src/virtual-session.mjs +20 -1
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?:
|
|
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?:
|
|
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?:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
// ============================================================================
|
|
@@ -833,6 +1286,21 @@ export class WshSession {
|
|
|
833
1286
|
/** Session capabilities advertised by OPEN_OK. */
|
|
834
1287
|
readonly capabilities: string[];
|
|
835
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* The server-assigned session_id this channel belongs to, from OPEN_OK
|
|
1291
|
+
* (clawser #48). `undefined` for channel kinds with no Attach/Resume-able
|
|
1292
|
+
* session (e.g. file channels). Pass to `WshClient.attachSession`/
|
|
1293
|
+
* `resumeSession` from another connection.
|
|
1294
|
+
*/
|
|
1295
|
+
readonly sessionId: string | undefined;
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* The session-scoped resume token minted at Open time, if any (clawser
|
|
1299
|
+
* #48). Pass to `WshClient.resumeSession` from a later connection to
|
|
1300
|
+
* reclaim this exact session.
|
|
1301
|
+
*/
|
|
1302
|
+
readonly resumeToken: Uint8Array | undefined;
|
|
1303
|
+
|
|
836
1304
|
/** Called when stdout/stderr data arrives. */
|
|
837
1305
|
onData: ((data: Uint8Array) => void) | null;
|
|
838
1306
|
|
|
@@ -866,6 +1334,9 @@ export class WshSession {
|
|
|
866
1334
|
/** Last terminal diff received for the session. */
|
|
867
1335
|
readonly lastTermDiff: Record<string, unknown> | null;
|
|
868
1336
|
|
|
1337
|
+
/** Whether `enableE2E()` has been called and E2E sealing is active for this session. */
|
|
1338
|
+
readonly e2eEnabled: boolean;
|
|
1339
|
+
|
|
869
1340
|
constructor(
|
|
870
1341
|
transport: WshTransport,
|
|
871
1342
|
channelId: number,
|
|
@@ -874,15 +1345,29 @@ export class WshSession {
|
|
|
874
1345
|
opts?: {
|
|
875
1346
|
dataMode?: 'stream' | 'virtual';
|
|
876
1347
|
capabilities?: string[];
|
|
1348
|
+
sessionId?: string;
|
|
1349
|
+
resumeToken?: Uint8Array;
|
|
877
1350
|
},
|
|
878
1351
|
);
|
|
879
1352
|
|
|
880
1353
|
/**
|
|
881
1354
|
* Write data to the session's stdin stream.
|
|
882
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).
|
|
883
1358
|
*/
|
|
884
1359
|
write(data: Uint8Array | string): Promise<void>;
|
|
885
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
|
+
|
|
886
1371
|
/**
|
|
887
1372
|
* Request the remote PTY to resize.
|
|
888
1373
|
*/
|
|
@@ -958,6 +1443,21 @@ export interface WshOpenSessionOptions {
|
|
|
958
1443
|
/** Options for WshClient.attachSession(). */
|
|
959
1444
|
export interface WshAttachSessionOptions {
|
|
960
1445
|
readOnly?: boolean;
|
|
1446
|
+
/**
|
|
1447
|
+
* Session-scoped token (clawser #48; optional). Omit for the common
|
|
1448
|
+
* case of attaching via ownership or a `grantSessionAccess` grant --
|
|
1449
|
+
* the server accepts EITHER a valid token OR the caller already
|
|
1450
|
+
* owning/being ACL-granted access. Pass one (e.g. from
|
|
1451
|
+
* `WshSession.resumeToken`) only if you have it.
|
|
1452
|
+
*/
|
|
1453
|
+
token?: Uint8Array;
|
|
1454
|
+
timeout?: number;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** Options for WshClient.resumeSession(). */
|
|
1458
|
+
export interface WshResumeSessionOptions {
|
|
1459
|
+
/** Last sequence number this client already has (currently advisory). */
|
|
1460
|
+
lastSeq?: number;
|
|
961
1461
|
timeout?: number;
|
|
962
1462
|
}
|
|
963
1463
|
|
|
@@ -1060,6 +1560,14 @@ export class WshClient {
|
|
|
1060
1560
|
/** Server-assigned session ID. */
|
|
1061
1561
|
readonly sessionId: string | null;
|
|
1062
1562
|
|
|
1563
|
+
/**
|
|
1564
|
+
* This connection's own AUTH-level token (from AUTH_OK), scoped to this
|
|
1565
|
+
* connection's auth session_id -- NOT a PTY/exec session token. See
|
|
1566
|
+
* `WshSession.resumeToken` for the per-session credential used by
|
|
1567
|
+
* `resumeSession`.
|
|
1568
|
+
*/
|
|
1569
|
+
readonly authToken: Uint8Array | null;
|
|
1570
|
+
|
|
1063
1571
|
/** Read-only view of active sessions. */
|
|
1064
1572
|
readonly sessions: Map<number, WshSession>;
|
|
1065
1573
|
|
|
@@ -1118,13 +1626,17 @@ export class WshClient {
|
|
|
1118
1626
|
|
|
1119
1627
|
/**
|
|
1120
1628
|
* Attach to an existing remote session (collaborative or read-only).
|
|
1629
|
+
* Authorization is satisfied by EITHER `opts.token` OR the caller
|
|
1630
|
+
* already owning/being ACL-granted access to `targetSessionId`
|
|
1631
|
+
* server-side -- see `WshAttachSessionOptions.token`.
|
|
1121
1632
|
*/
|
|
1122
1633
|
attachSession(targetSessionId: string, opts?: WshAttachSessionOptions): Promise<WshMessage>;
|
|
1123
1634
|
|
|
1124
1635
|
/**
|
|
1125
|
-
* Resume a previously disconnected session.
|
|
1636
|
+
* Resume a previously disconnected session. Unlike `attachSession()`,
|
|
1637
|
+
* `token` is required and verified unconditionally server-side.
|
|
1126
1638
|
*/
|
|
1127
|
-
resumeSession(targetSessionId: string, token:
|
|
1639
|
+
resumeSession(targetSessionId: string, token: Uint8Array, opts?: WshResumeSessionOptions): Promise<WshMessage>;
|
|
1128
1640
|
|
|
1129
1641
|
/**
|
|
1130
1642
|
* Detach from a remote session, leaving it running server-side.
|
|
@@ -1213,7 +1725,13 @@ export class WshClient {
|
|
|
1213
1725
|
/**
|
|
1214
1726
|
* Download a file from a remote path.
|
|
1215
1727
|
*/
|
|
1216
|
-
download(
|
|
1728
|
+
download(
|
|
1729
|
+
remotePath: string,
|
|
1730
|
+
opts?: {
|
|
1731
|
+
onProgress?: (progress: { received: number; total: number }) => void;
|
|
1732
|
+
timeout?: number;
|
|
1733
|
+
},
|
|
1734
|
+
): Promise<Uint8Array>;
|
|
1217
1735
|
|
|
1218
1736
|
/**
|
|
1219
1737
|
* Discover MCP tools available on the remote server.
|