@johnhenry/wsh 0.7.0 → 0.13.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/package.json +4 -1
- package/src/auth.mjs +106 -0
- package/src/client.mjs +491 -174
- package/src/file-transfer.mjs +39 -166
- package/src/index.d.ts +110 -0
- package/src/index.mjs +13 -0
- package/src/messages.gen.mjs +66 -10
- package/src/messages.mjs +1 -1
- package/src/mlkem.mjs +144 -0
- package/src/qmux-connection.mjs +698 -0
- package/src/qmux.mjs +620 -0
- package/src/session.mjs +62 -0
- package/src/transport-ws.mjs +145 -352
package/src/file-transfer.mjs
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WshFileTransfer —
|
|
2
|
+
* WshFileTransfer — scp-like file transfer over a wsh connection.
|
|
3
3
|
*
|
|
4
|
-
* Uploads and downloads
|
|
5
|
-
*
|
|
4
|
+
* Uploads and downloads delegate to the underlying client's upload()/
|
|
5
|
+
* download() (FileChunk control messages over a 'file'-kind channel —
|
|
6
|
+
* see client.mjs). list() is a separate helper that runs `ls -la` over
|
|
7
|
+
* a plain exec channel.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { MSG, open, CHANNEL_KIND } from './messages.mjs';
|
|
9
11
|
|
|
10
|
-
/** Transfer chunk size: 64KB. */
|
|
11
|
-
const CHUNK_SIZE = 65536;
|
|
12
|
-
|
|
13
12
|
/** Default timeout for waiting on control messages (30 seconds). */
|
|
14
13
|
const RESPONSE_TIMEOUT_MS = 30_000;
|
|
15
14
|
|
|
16
15
|
export class WshFileTransfer {
|
|
17
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* @type {{
|
|
18
|
+
* upload: function, download: function,
|
|
19
|
+
* sendControl: function, openStream: function, onControl: function,
|
|
20
|
+
* }}
|
|
21
|
+
*/
|
|
18
22
|
#client;
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* @param {object} client - A WshClient or any
|
|
25
|
+
* @param {object} client - A WshClient (or any object exposing upload()/
|
|
26
|
+
* download() with the same signatures — those are used directly for
|
|
27
|
+
* upload/download). list() additionally needs, for the underlying exec
|
|
28
|
+
* channel it runs `ls -la` over:
|
|
22
29
|
* - sendControl(msg): send a control message
|
|
23
30
|
* - openStream(): open a new bidirectional stream
|
|
24
31
|
* - onControl: settable callback for incoming control messages
|
|
@@ -32,12 +39,10 @@ export class WshFileTransfer {
|
|
|
32
39
|
/**
|
|
33
40
|
* Upload data to a remote path.
|
|
34
41
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* 4. Close the write side of the stream
|
|
40
|
-
* 5. Wait for EXIT message with status code
|
|
42
|
+
* Delegates to the underlying client's `upload()`, which sends the data
|
|
43
|
+
* as a sequence of `FileChunk` control messages over a `'file'`-kind
|
|
44
|
+
* channel — the single wire scheme for file transfer (see
|
|
45
|
+
* `WshClient.upload` in `client.mjs`).
|
|
41
46
|
*
|
|
42
47
|
* @param {Uint8Array | ArrayBuffer} data - File content to upload
|
|
43
48
|
* @param {string} remotePath - Destination path on the remote host
|
|
@@ -47,97 +52,31 @@ export class WshFileTransfer {
|
|
|
47
52
|
* @returns {Promise<{ success: boolean, bytesTransferred: number }>}
|
|
48
53
|
*/
|
|
49
54
|
async upload(data, remotePath, { onProgress, timeout = RESPONSE_TIMEOUT_MS } = {}) {
|
|
50
|
-
if (typeof this.#client.upload
|
|
51
|
-
|
|
52
|
-
onProgress: (value) => {
|
|
53
|
-
if (typeof value === 'number') {
|
|
54
|
-
onProgress?.({ sent: value, total: data.byteLength ?? data.length ?? 0 });
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
onProgress?.(value);
|
|
58
|
-
},
|
|
59
|
-
timeout,
|
|
60
|
-
});
|
|
61
|
-
const bytes = data instanceof Uint8Array ? data.byteLength : data.byteLength ?? data.length ?? 0;
|
|
62
|
-
return { success: true, bytesTransferred: bytes };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
66
|
-
if (!(bytes instanceof Uint8Array)) {
|
|
67
|
-
throw new TypeError('Data must be a Uint8Array or ArrayBuffer');
|
|
68
|
-
}
|
|
69
|
-
if (!remotePath || typeof remotePath !== 'string') {
|
|
70
|
-
throw new Error('remotePath is required');
|
|
55
|
+
if (typeof this.#client.upload !== 'function') {
|
|
56
|
+
throw new Error('WshFileTransfer requires a client exposing upload() (e.g. WshClient)');
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
await this.#client.upload(data, remotePath, {
|
|
60
|
+
onProgress: (value) => {
|
|
61
|
+
if (typeof value === 'number') {
|
|
62
|
+
onProgress?.({ sent: value, total: data.byteLength ?? data.length ?? 0 });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onProgress?.(value);
|
|
66
|
+
},
|
|
67
|
+
timeout,
|
|
77
68
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await this.#client.sendControl(openMsg);
|
|
82
|
-
|
|
83
|
-
// 2. Wait for OPEN_OK or OPEN_FAIL
|
|
84
|
-
const response = await this._waitForMessage(
|
|
85
|
-
(msg) => msg.type === MSG.OPEN_OK || msg.type === MSG.OPEN_FAIL,
|
|
86
|
-
timeout
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
if (response.type === MSG.OPEN_FAIL) {
|
|
90
|
-
throw new Error(`Upload rejected: ${response.reason || 'unknown reason'}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const channelId = response.channel_id;
|
|
94
|
-
|
|
95
|
-
// 3. Open a dedicated data stream and write in chunks
|
|
96
|
-
const stream = await this.#client.openStream();
|
|
97
|
-
const writer = stream.writable.getWriter();
|
|
98
|
-
let sent = 0;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
while (sent < bytes.length) {
|
|
102
|
-
const end = Math.min(sent + CHUNK_SIZE, bytes.length);
|
|
103
|
-
const chunk = bytes.subarray(sent, end);
|
|
104
|
-
await writer.write(chunk);
|
|
105
|
-
sent = end;
|
|
106
|
-
try {
|
|
107
|
-
onProgress?.({ sent, total: bytes.length });
|
|
108
|
-
} catch { /* ignore callback errors */ }
|
|
109
|
-
}
|
|
110
|
-
} finally {
|
|
111
|
-
// 4. Close the write side to signal end of data
|
|
112
|
-
try {
|
|
113
|
-
await writer.close();
|
|
114
|
-
} catch {
|
|
115
|
-
// Stream may already be closed on error
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 5. Wait for EXIT confirmation
|
|
120
|
-
const exitMsg = await this._waitForMessage(
|
|
121
|
-
(msg) => msg.type === MSG.EXIT && msg.channel_id === channelId,
|
|
122
|
-
timeout
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const success = exitMsg.code === 0;
|
|
126
|
-
if (!success) {
|
|
127
|
-
throw new Error(`Upload failed with exit code ${exitMsg.code}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { success, bytesTransferred: sent };
|
|
69
|
+
const bytes = data instanceof Uint8Array ? data.byteLength : data.byteLength ?? data.length ?? 0;
|
|
70
|
+
return { success: true, bytesTransferred: bytes };
|
|
131
71
|
}
|
|
132
72
|
|
|
133
73
|
/**
|
|
134
74
|
* Download a file from a remote path.
|
|
135
75
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* 4. Return complete file content as Uint8Array
|
|
76
|
+
* Delegates to the underlying client's `download()`, which reads the
|
|
77
|
+
* data as a sequence of `FileChunk` control messages over a
|
|
78
|
+
* `'file'`-kind channel — the single wire scheme for file transfer (see
|
|
79
|
+
* `WshClient.download` in `client.mjs`).
|
|
141
80
|
*
|
|
142
81
|
* @param {string} remotePath - File path on the remote host
|
|
143
82
|
* @param {object} [opts]
|
|
@@ -146,76 +85,10 @@ export class WshFileTransfer {
|
|
|
146
85
|
* @returns {Promise<Uint8Array>} File content
|
|
147
86
|
*/
|
|
148
87
|
async download(remotePath, { onProgress, timeout = RESPONSE_TIMEOUT_MS } = {}) {
|
|
149
|
-
if (typeof this.#client.download
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!remotePath || typeof remotePath !== 'string') {
|
|
154
|
-
throw new Error('remotePath is required');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 1. Request a file download channel
|
|
158
|
-
const openMsg = open({
|
|
159
|
-
kind: CHANNEL_KIND.FILE,
|
|
160
|
-
command: 'download',
|
|
161
|
-
});
|
|
162
|
-
openMsg.path = remotePath;
|
|
163
|
-
|
|
164
|
-
await this.#client.sendControl(openMsg);
|
|
165
|
-
|
|
166
|
-
// 2. Wait for OPEN_OK or OPEN_FAIL
|
|
167
|
-
const response = await this._waitForMessage(
|
|
168
|
-
(msg) => msg.type === MSG.OPEN_OK || msg.type === MSG.OPEN_FAIL,
|
|
169
|
-
timeout
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
if (response.type === MSG.OPEN_FAIL) {
|
|
173
|
-
throw new Error(`Download rejected: ${response.reason || 'unknown reason'}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const channelId = response.channel_id;
|
|
177
|
-
const totalSize = response.size; // Server may include total file size
|
|
178
|
-
|
|
179
|
-
// 3. Open a dedicated data stream and read chunks
|
|
180
|
-
const stream = await this.#client.openStream();
|
|
181
|
-
const reader = stream.readable.getReader();
|
|
182
|
-
const chunks = [];
|
|
183
|
-
let received = 0;
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
while (true) {
|
|
187
|
-
const { done, value } = await reader.read();
|
|
188
|
-
if (done) break;
|
|
189
|
-
|
|
190
|
-
chunks.push(value);
|
|
191
|
-
received += value.length;
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
onProgress?.({ received, total: totalSize });
|
|
195
|
-
} catch { /* ignore callback errors */ }
|
|
196
|
-
}
|
|
197
|
-
} finally {
|
|
198
|
-
reader.releaseLock();
|
|
88
|
+
if (typeof this.#client.download !== 'function') {
|
|
89
|
+
throw new Error('WshFileTransfer requires a client exposing download() (e.g. WshClient)');
|
|
199
90
|
}
|
|
200
|
-
|
|
201
|
-
// 4. Wait for EXIT confirmation
|
|
202
|
-
try {
|
|
203
|
-
const exitMsg = await this._waitForMessage(
|
|
204
|
-
(msg) => msg.type === MSG.EXIT && msg.channel_id === channelId,
|
|
205
|
-
timeout
|
|
206
|
-
);
|
|
207
|
-
if (exitMsg.code !== 0) {
|
|
208
|
-
throw new Error(`Download failed with exit code ${exitMsg.code}`);
|
|
209
|
-
}
|
|
210
|
-
} catch (err) {
|
|
211
|
-
// If the stream closed cleanly and we got data, the EXIT may have
|
|
212
|
-
// already been consumed or the server may not send one for downloads.
|
|
213
|
-
// Only rethrow if we have no data.
|
|
214
|
-
if (received === 0) throw err;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Concatenate chunks into a single Uint8Array
|
|
218
|
-
return this._concatChunks(chunks, received);
|
|
91
|
+
return await this.#client.download(remotePath, { onProgress, timeout });
|
|
219
92
|
}
|
|
220
93
|
|
|
221
94
|
/**
|
package/src/index.d.ts
CHANGED
|
@@ -421,6 +421,7 @@ export function reversePeers(opts?: {
|
|
|
421
421
|
export function reverseConnect(opts?: {
|
|
422
422
|
targetFingerprint?: string;
|
|
423
423
|
username?: string;
|
|
424
|
+
fromFingerprint?: string;
|
|
424
425
|
}): WshMessage;
|
|
425
426
|
|
|
426
427
|
export function reverseAccept(opts?: {
|
|
@@ -441,6 +442,16 @@ export function reverseReject(opts?: {
|
|
|
441
442
|
reason?: string;
|
|
442
443
|
}): WshMessage;
|
|
443
444
|
|
|
445
|
+
export function relayForward(opts?: {
|
|
446
|
+
fromFingerprint?: string;
|
|
447
|
+
inner?: Uint8Array;
|
|
448
|
+
}): WshMessage;
|
|
449
|
+
|
|
450
|
+
/** Message types a relay may forward inside a RelayForward wrapper. */
|
|
451
|
+
export const RELAY_FORWARDABLE: ReadonlySet<number>;
|
|
452
|
+
|
|
453
|
+
export function isRelayForwardable(typeNum: number): boolean;
|
|
454
|
+
|
|
444
455
|
// -- Message constructors (gateway) --
|
|
445
456
|
|
|
446
457
|
export function openTcp(opts?: {
|
|
@@ -622,6 +633,37 @@ export function verifyChallenge(
|
|
|
622
633
|
channelBinding?: Uint8Array,
|
|
623
634
|
): Promise<boolean>;
|
|
624
635
|
|
|
636
|
+
/** Fields of a signed peer record (reverse-mode registration). See `signPeerRecord`. */
|
|
637
|
+
export interface WshPeerRecord {
|
|
638
|
+
username: string;
|
|
639
|
+
peerType?: string;
|
|
640
|
+
shellBackend?: string;
|
|
641
|
+
capabilities?: string[];
|
|
642
|
+
supportsAttach?: boolean;
|
|
643
|
+
supportsReplay?: boolean;
|
|
644
|
+
supportsEcho?: boolean;
|
|
645
|
+
supportsTermSync?: boolean;
|
|
646
|
+
/** The signing peer's own monotonic counter (current-time-millis in practice). */
|
|
647
|
+
seq: number;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Build the signed transcript for a peer record -- domain-separated from
|
|
652
|
+
* `buildTranscript`'s auth-challenge transcript even under the same key.
|
|
653
|
+
* @returns 32-byte SHA-256 hash
|
|
654
|
+
*/
|
|
655
|
+
export function buildPeerRecordTranscript(record: WshPeerRecord): Promise<Uint8Array>;
|
|
656
|
+
|
|
657
|
+
/** Client-side: sign a peer record with the identity key pair. */
|
|
658
|
+
export function signPeerRecord(
|
|
659
|
+
privateKey: CryptoKey,
|
|
660
|
+
publicKey: CryptoKey,
|
|
661
|
+
record: WshPeerRecord,
|
|
662
|
+
): Promise<{ signature: Uint8Array; publicKeyRaw: Uint8Array }>;
|
|
663
|
+
|
|
664
|
+
/** Verify a peer record's signature against the claimed record fields. */
|
|
665
|
+
export function verifyPeerRecord(publicKey: CryptoKey, signature: Uint8Array, record: WshPeerRecord): Promise<boolean>;
|
|
666
|
+
|
|
625
667
|
/**
|
|
626
668
|
* Compute the SHA-256 fingerprint of a raw public key.
|
|
627
669
|
* @returns hex-encoded fingerprint
|
|
@@ -973,6 +1015,33 @@ export interface WshPeerInfo {
|
|
|
973
1015
|
supports_echo: boolean;
|
|
974
1016
|
supports_term_sync: boolean;
|
|
975
1017
|
last_seen: number | null;
|
|
1018
|
+
/** Raw 32-byte Ed25519 public key from the peer's signed record, if present. */
|
|
1019
|
+
public_key?: Uint8Array;
|
|
1020
|
+
/** The signing peer's own monotonic counter, from the signed record. */
|
|
1021
|
+
seq?: number;
|
|
1022
|
+
/** Raw Ed25519 signature over the peer record's transcript, if present. */
|
|
1023
|
+
record_signature?: Uint8Array;
|
|
1024
|
+
/**
|
|
1025
|
+
* Not a wire field -- computed client-side by `listPeers()` from
|
|
1026
|
+
* `public_key`/`seq`/`record_signature` via `verifyPeerRecord`. `true`
|
|
1027
|
+
* only if the signature verifies AND the key's fingerprint matches
|
|
1028
|
+
* `fingerprint`; `false` for missing/tampered/mismatched records.
|
|
1029
|
+
*/
|
|
1030
|
+
verified: boolean;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Result from WshClient.initiateE2E(). */
|
|
1034
|
+
export interface WshE2EResult {
|
|
1035
|
+
/** Non-extractable AES-256-GCM key derived from the exchange. */
|
|
1036
|
+
sharedSecret: CryptoKey;
|
|
1037
|
+
/** The peer's ephemeral X25519 public key. */
|
|
1038
|
+
peerPublicKey: Uint8Array;
|
|
1039
|
+
/**
|
|
1040
|
+
* Whether hybrid X25519+ML-KEM-768 mode actually took effect --
|
|
1041
|
+
* `false` if `algorithm: 'X25519'` was requested, or if hybrid was
|
|
1042
|
+
* requested but the peer didn't support it (automatic fallback).
|
|
1043
|
+
*/
|
|
1044
|
+
hybrid: boolean;
|
|
976
1045
|
}
|
|
977
1046
|
|
|
978
1047
|
/**
|
|
@@ -1057,6 +1126,27 @@ export class WshClient {
|
|
|
1057
1126
|
*/
|
|
1058
1127
|
resumeSession(targetSessionId: string, token: string, opts?: { timeout?: number }): Promise<WshMessage>;
|
|
1059
1128
|
|
|
1129
|
+
/**
|
|
1130
|
+
* Detach from a remote session, leaving it running server-side.
|
|
1131
|
+
*/
|
|
1132
|
+
detach(sessionId: string, timeout?: number): Promise<void>;
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* List sessions on the server this connection's key can see (a server
|
|
1136
|
+
* round trip — distinct from the purely local listSessions()).
|
|
1137
|
+
*/
|
|
1138
|
+
listRemoteSessions(timeout?: number): Promise<object[]>;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Grant another principal access to a session this connection owns.
|
|
1142
|
+
*/
|
|
1143
|
+
grantSessionAccess(sessionId: string, principal: string, permissions?: string[]): Promise<void>;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Revoke a previously granted principal's access to a session.
|
|
1147
|
+
*/
|
|
1148
|
+
revokeSessionAccess(sessionId: string, principal: string, reason?: string): Promise<void>;
|
|
1149
|
+
|
|
1060
1150
|
/**
|
|
1061
1151
|
* Gracefully disconnect: close all sessions and the transport.
|
|
1062
1152
|
*/
|
|
@@ -1095,6 +1185,26 @@ export class WshClient {
|
|
|
1095
1185
|
*/
|
|
1096
1186
|
sendRelayControl(msg: WshMessage): Promise<void>;
|
|
1097
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* Initiate end-to-end encryption for a session: `'X25519'` (default,
|
|
1190
|
+
* classical ECDH) or `'X25519+ML-KEM-768'` (hybrid, with automatic
|
|
1191
|
+
* fallback to classical if the peer doesn't support it). Experimental
|
|
1192
|
+
* -- the derived key is not yet wired to any actual encryption.
|
|
1193
|
+
*/
|
|
1194
|
+
initiateE2E(sessionId: string, algorithm?: 'X25519' | 'X25519+ML-KEM-768', timeout?: number): Promise<WshE2EResult>;
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Mark a peer fingerprint as an accepted reverse-connect bridge partner.
|
|
1198
|
+
* Only RelayForward-wrapped messages whose from_fingerprint is trusted
|
|
1199
|
+
* this way are unwrapped and delivered via onRelayMessage.
|
|
1200
|
+
*/
|
|
1201
|
+
trustRelayPeer(fingerprint: string): void;
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Stop trusting a peer as a relay-forward bridge partner.
|
|
1205
|
+
*/
|
|
1206
|
+
untrustRelayPeer(fingerprint: string): void;
|
|
1207
|
+
|
|
1098
1208
|
/**
|
|
1099
1209
|
* Upload a blob to a remote path.
|
|
1100
1210
|
*/
|
package/src/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
presence, controlChanged, metrics,
|
|
18
18
|
mcpDiscover, mcpTools, mcpCall, mcpResult,
|
|
19
19
|
reverseRegister, reverseList, reversePeers, reverseConnect, reverseAccept, reverseReject,
|
|
20
|
+
relayForward, RELAY_FORWARDABLE, isRelayForwardable,
|
|
20
21
|
openTcp, openUdp, resolveDns, gatewayOk, gatewayFail, gatewayClose,
|
|
21
22
|
inboundOpen, inboundAccept, inboundReject, dnsResult,
|
|
22
23
|
listenRequest, listenOk, listenFail, listenClose, gatewayData,
|
|
@@ -44,6 +45,7 @@ export {
|
|
|
44
45
|
generateKeyPair, exportPublicKeyRaw, exportPublicKeySSH,
|
|
45
46
|
importPublicKeyRaw, exportPrivateKeyPKCS8, importPrivateKeyPKCS8,
|
|
46
47
|
sign, verify, buildTranscript, signChallenge, verifyChallenge,
|
|
48
|
+
buildPeerRecordTranscript, signPeerRecord, verifyPeerRecord,
|
|
47
49
|
fingerprint, shortFingerprint, generateNonce,
|
|
48
50
|
parseSSHPublicKey, extractRawFromSSHWire, base64Decode,
|
|
49
51
|
} from './auth.mjs';
|
|
@@ -52,6 +54,17 @@ export {
|
|
|
52
54
|
export { WshTransport, WebTransportTransport, dispatchSerially, SerialQueue } from './transport.mjs';
|
|
53
55
|
export { WebSocketTransport, WS_FRAME_TYPE } from './transport-ws.mjs';
|
|
54
56
|
|
|
57
|
+
// QMux (QUIC-v1 frames over an ordered byte stream, draft-ietf-quic-qmux-02)
|
|
58
|
+
// -- the wire multiplexing layer WebSocketTransport speaks. Exposed so an
|
|
59
|
+
// alternate server implementation (not just the Rust wsh-server, which
|
|
60
|
+
// vendors its own port) can speak the same framing without reimplementing
|
|
61
|
+
// it -- see clawser's tools/wsh-server.mjs for exactly this use case.
|
|
62
|
+
export { QMuxConnection, DEFAULTS as QMUX_DEFAULTS } from './qmux-connection.mjs';
|
|
63
|
+
export {
|
|
64
|
+
ERROR_CODE as QMUX_ERROR_CODE, STREAM_INITIATOR as QMUX_STREAM_INITIATOR,
|
|
65
|
+
firstBidiStreamId, nextBidiStreamId, isClientInitiated, isBidirectional,
|
|
66
|
+
} from './qmux.mjs';
|
|
67
|
+
|
|
55
68
|
// Session + Client
|
|
56
69
|
export { WshSession } from './session.mjs';
|
|
57
70
|
export { WshVirtualSessionBackend, normalizeSessionData } from './virtual-session.mjs';
|
package/src/messages.gen.mjs
CHANGED
|
@@ -62,6 +62,7 @@ export const MSG = Object.freeze({
|
|
|
62
62
|
REVERSE_CONNECT: 0x53,
|
|
63
63
|
REVERSE_ACCEPT: 0x54,
|
|
64
64
|
REVERSE_REJECT: 0x55,
|
|
65
|
+
RELAY_FORWARD: 0x56,
|
|
65
66
|
|
|
66
67
|
// Session
|
|
67
68
|
SESSION_LIST: 0x5f,
|
|
@@ -457,7 +458,7 @@ export function mcpResult({ result } = {}) {
|
|
|
457
458
|
};
|
|
458
459
|
}
|
|
459
460
|
|
|
460
|
-
export function reverseRegister({ username, capabilities = [], peerType = "host", shellBackend = "pty", supportsAttach = false, supportsReplay = false, supportsEcho = false, supportsTermSync = false, publicKey } = {}) {
|
|
461
|
+
export function reverseRegister({ username, capabilities = [], peerType = "host", shellBackend = "pty", supportsAttach = false, supportsReplay = false, supportsEcho = false, supportsTermSync = false, publicKey, seq, recordSignature } = {}) {
|
|
461
462
|
return {
|
|
462
463
|
type: MSG.REVERSE_REGISTER,
|
|
463
464
|
username,
|
|
@@ -469,6 +470,8 @@ export function reverseRegister({ username, capabilities = [], peerType = "host"
|
|
|
469
470
|
supports_echo: supportsEcho,
|
|
470
471
|
supports_term_sync: supportsTermSync,
|
|
471
472
|
public_key: publicKey,
|
|
473
|
+
seq,
|
|
474
|
+
record_signature: recordSignature,
|
|
472
475
|
};
|
|
473
476
|
}
|
|
474
477
|
|
|
@@ -483,11 +486,12 @@ export function reversePeers({ peers } = {}) {
|
|
|
483
486
|
};
|
|
484
487
|
}
|
|
485
488
|
|
|
486
|
-
export function reverseConnect({ targetFingerprint, username } = {}) {
|
|
489
|
+
export function reverseConnect({ targetFingerprint, username, fromFingerprint } = {}) {
|
|
487
490
|
return {
|
|
488
491
|
type: MSG.REVERSE_CONNECT,
|
|
489
492
|
target_fingerprint: targetFingerprint,
|
|
490
493
|
username,
|
|
494
|
+
from_fingerprint: fromFingerprint,
|
|
491
495
|
};
|
|
492
496
|
}
|
|
493
497
|
|
|
@@ -515,6 +519,14 @@ export function reverseReject({ targetFingerprint, username, reason } = {}) {
|
|
|
515
519
|
};
|
|
516
520
|
}
|
|
517
521
|
|
|
522
|
+
export function relayForward({ fromFingerprint, inner } = {}) {
|
|
523
|
+
return {
|
|
524
|
+
type: MSG.RELAY_FORWARD,
|
|
525
|
+
from_fingerprint: fromFingerprint,
|
|
526
|
+
inner,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
518
530
|
export function sessionList({ sessions } = {}) {
|
|
519
531
|
return {
|
|
520
532
|
type: MSG.SESSION_LIST,
|
|
@@ -759,13 +771,12 @@ export function copilotDetach({ sessionId, reason } = {}) {
|
|
|
759
771
|
return msg;
|
|
760
772
|
}
|
|
761
773
|
|
|
762
|
-
export function keyExchange({ algorithm, publicKey, sessionId } = {}) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
};
|
|
774
|
+
export function keyExchange({ algorithm, publicKey, sessionId, kemPublicKey, kemCiphertext } = {}) {
|
|
775
|
+
const msg = { type: MSG.KEY_EXCHANGE, algorithm, session_id: sessionId };
|
|
776
|
+
if (publicKey !== undefined) msg.public_key = publicKey;
|
|
777
|
+
if (kemPublicKey !== undefined) msg.kem_public_key = kemPublicKey;
|
|
778
|
+
if (kemCiphertext !== undefined) msg.kem_ciphertext = kemCiphertext;
|
|
779
|
+
return msg;
|
|
769
780
|
}
|
|
770
781
|
|
|
771
782
|
export function encryptedFrame({ nonce, ciphertext, sessionId } = {}) {
|
|
@@ -859,13 +870,14 @@ export function fileResult({ channelId, success, metadata = {}, errorMessage } =
|
|
|
859
870
|
return msg;
|
|
860
871
|
}
|
|
861
872
|
|
|
862
|
-
export function fileChunk({ channelId, offset, data, isFinal } = {}) {
|
|
873
|
+
export function fileChunk({ channelId, offset, data, isFinal, totalSize } = {}) {
|
|
863
874
|
return {
|
|
864
875
|
type: MSG.FILE_CHUNK,
|
|
865
876
|
channel_id: channelId,
|
|
866
877
|
offset,
|
|
867
878
|
data,
|
|
868
879
|
is_final: isFinal,
|
|
880
|
+
total_size: totalSize,
|
|
869
881
|
};
|
|
870
882
|
}
|
|
871
883
|
|
|
@@ -903,6 +915,50 @@ export function terminalConfig({ channelId, frontend, options = {} } = {}) {
|
|
|
903
915
|
};
|
|
904
916
|
}
|
|
905
917
|
|
|
918
|
+
// ── Relay forwarding ─────────────────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
// Message types a relay may forward inside a RelayForward wrapper.
|
|
921
|
+
export const RELAY_FORWARDABLE = new Set([
|
|
922
|
+
MSG.OPEN,
|
|
923
|
+
MSG.OPEN_OK,
|
|
924
|
+
MSG.OPEN_FAIL,
|
|
925
|
+
MSG.CLOSE,
|
|
926
|
+
MSG.EXIT,
|
|
927
|
+
MSG.RESIZE,
|
|
928
|
+
MSG.SIGNAL,
|
|
929
|
+
MSG.SESSION_DATA,
|
|
930
|
+
MSG.GATEWAY_DATA,
|
|
931
|
+
MSG.GATEWAY_OK,
|
|
932
|
+
MSG.GATEWAY_FAIL,
|
|
933
|
+
MSG.GATEWAY_CLOSE,
|
|
934
|
+
MSG.MCP_DISCOVER,
|
|
935
|
+
MSG.MCP_TOOLS,
|
|
936
|
+
MSG.MCP_CALL,
|
|
937
|
+
MSG.MCP_RESULT,
|
|
938
|
+
MSG.REVERSE_ACCEPT,
|
|
939
|
+
MSG.REVERSE_REJECT,
|
|
940
|
+
MSG.GUEST_JOIN,
|
|
941
|
+
MSG.GUEST_REVOKE,
|
|
942
|
+
MSG.COPILOT_ATTACH,
|
|
943
|
+
MSG.COPILOT_DETACH,
|
|
944
|
+
MSG.FILE_OP,
|
|
945
|
+
MSG.FILE_RESULT,
|
|
946
|
+
MSG.FILE_CHUNK,
|
|
947
|
+
MSG.POLICY_EVAL,
|
|
948
|
+
MSG.ECHO_ACK,
|
|
949
|
+
MSG.ECHO_STATE,
|
|
950
|
+
MSG.TERM_SYNC,
|
|
951
|
+
MSG.TERM_DIFF,
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* @param {number} typeNum
|
|
956
|
+
* @returns {boolean}
|
|
957
|
+
*/
|
|
958
|
+
export function isRelayForwardable(typeNum) {
|
|
959
|
+
return RELAY_FORWARDABLE.has(typeNum);
|
|
960
|
+
}
|
|
961
|
+
|
|
906
962
|
// ── Utility ───────────────────────────────────────────────────────────
|
|
907
963
|
|
|
908
964
|
/**
|
package/src/messages.mjs
CHANGED