@johnhenry/wsh 0.7.0 → 0.14.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 +549 -189
- package/src/file-transfer.mjs +39 -166
- package/src/index.d.ts +156 -2
- package/src/index.mjs +13 -0
- package/src/messages.gen.mjs +73 -19
- 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 +107 -1
- package/src/transport-ws.mjs +145 -352
package/src/session.mjs
CHANGED
|
@@ -44,6 +44,23 @@ export class WshSession {
|
|
|
44
44
|
/** @type {string[]} Advertised session capabilities. */
|
|
45
45
|
#capabilities = [];
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The server-assigned session_id this channel belongs to, from OPEN_OK
|
|
49
|
+
* (clawser #48). `undefined` for channel kinds with no Attach/Resume-able
|
|
50
|
+
* session (e.g. file channels).
|
|
51
|
+
* @type {string|undefined}
|
|
52
|
+
*/
|
|
53
|
+
#sessionId;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The session-scoped HMAC token minted by the server at Open time
|
|
57
|
+
* (clawser #48), also from OPEN_OK. Pass it to `WshClient.resumeSession`
|
|
58
|
+
* (or optionally `attachSession`) from a later connection to reclaim
|
|
59
|
+
* this exact session. `undefined` alongside `#sessionId`.
|
|
60
|
+
* @type {Uint8Array|undefined}
|
|
61
|
+
*/
|
|
62
|
+
#resumeToken;
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
65
|
* Stream IDs returned by the server in OPEN_OK.
|
|
49
66
|
* Typically { stdin: number, stdout: number } or a single bidirectional ID.
|
|
@@ -131,6 +148,16 @@ export class WshSession {
|
|
|
131
148
|
/** @type {object|null} */
|
|
132
149
|
#lastTermDiff = null;
|
|
133
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Queue of received FileChunk payloads not yet consumed by
|
|
153
|
+
* _readFileChunk(). 'file'-kind sessions only.
|
|
154
|
+
* @type {Array<object>}
|
|
155
|
+
*/
|
|
156
|
+
#fileChunkQueue = [];
|
|
157
|
+
|
|
158
|
+
/** @type {{resolve: function, reject: function}|null} */
|
|
159
|
+
#fileChunkWaiter = null;
|
|
160
|
+
|
|
134
161
|
// ── Constructor ─────────────────────────────────────────────────────
|
|
135
162
|
|
|
136
163
|
/**
|
|
@@ -141,8 +168,13 @@ export class WshSession {
|
|
|
141
168
|
* @param {object} [opts]
|
|
142
169
|
* @param {'stream'|'virtual'} [opts.dataMode='stream']
|
|
143
170
|
* @param {string[]} [opts.capabilities=[]]
|
|
171
|
+
* @param {string} [opts.sessionId] - Server session_id from OPEN_OK (clawser #48; pty/exec only)
|
|
172
|
+
* @param {Uint8Array} [opts.resumeToken] - Session-scoped resume token from OPEN_OK (clawser #48)
|
|
144
173
|
*/
|
|
145
|
-
constructor(
|
|
174
|
+
constructor(
|
|
175
|
+
transport, channelId, streamIds, kind,
|
|
176
|
+
{ dataMode = 'stream', capabilities = [], sessionId, resumeToken } = {}
|
|
177
|
+
) {
|
|
146
178
|
this.#transport = transport;
|
|
147
179
|
this.channelId = channelId;
|
|
148
180
|
this.#streamIds = streamIds;
|
|
@@ -150,6 +182,28 @@ export class WshSession {
|
|
|
150
182
|
this.id = String(channelId);
|
|
151
183
|
this.#dataMode = dataMode === 'virtual' ? 'virtual' : 'stream';
|
|
152
184
|
this.#capabilities = Array.isArray(capabilities) ? [...capabilities] : [];
|
|
185
|
+
this.#sessionId = sessionId;
|
|
186
|
+
this.#resumeToken = resumeToken;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The server-assigned session_id this channel belongs to, if any
|
|
191
|
+
* (clawser #48; `undefined` for channel kinds with no Attach/Resume-able
|
|
192
|
+
* session, e.g. file channels). Pass to `WshClient.attachSession`/
|
|
193
|
+
* `resumeSession` from another connection.
|
|
194
|
+
*/
|
|
195
|
+
get sessionId() {
|
|
196
|
+
return this.#sessionId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The session-scoped resume token minted at Open time, if any (clawser
|
|
201
|
+
* #48). Pass it to `WshClient.resumeSession` from a later connection to
|
|
202
|
+
* reclaim this exact session, proving you're the same credentialed
|
|
203
|
+
* opener rather than merely an authorized principal.
|
|
204
|
+
*/
|
|
205
|
+
get resumeToken() {
|
|
206
|
+
return this.#resumeToken;
|
|
153
207
|
}
|
|
154
208
|
|
|
155
209
|
/** Current session state. */
|
|
@@ -358,6 +412,7 @@ export class WshSession {
|
|
|
358
412
|
|
|
359
413
|
this.#stdinWriter = null;
|
|
360
414
|
this.#stdoutReadable = null;
|
|
415
|
+
this.#resolvePendingFileChunk(null);
|
|
361
416
|
this.#emitClose();
|
|
362
417
|
}
|
|
363
418
|
|
|
@@ -382,6 +437,17 @@ export class WshSession {
|
|
|
382
437
|
break;
|
|
383
438
|
}
|
|
384
439
|
|
|
440
|
+
case MSG.FILE_CHUNK: {
|
|
441
|
+
if (this.#fileChunkWaiter) {
|
|
442
|
+
const { resolve } = this.#fileChunkWaiter;
|
|
443
|
+
this.#fileChunkWaiter = null;
|
|
444
|
+
resolve(msg);
|
|
445
|
+
} else {
|
|
446
|
+
this.#fileChunkQueue.push(msg);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
|
|
385
451
|
case MSG.EXIT: {
|
|
386
452
|
this.#exitCode = msg.code ?? -1;
|
|
387
453
|
try {
|
|
@@ -399,6 +465,7 @@ export class WshSession {
|
|
|
399
465
|
this.#abort.abort();
|
|
400
466
|
this.#virtualBackend?.close();
|
|
401
467
|
this.#releaseStreams();
|
|
468
|
+
this.#resolvePendingFileChunk(null);
|
|
402
469
|
this.#emitClose();
|
|
403
470
|
}
|
|
404
471
|
break;
|
|
@@ -469,6 +536,45 @@ export class WshSession {
|
|
|
469
536
|
}
|
|
470
537
|
}
|
|
471
538
|
|
|
539
|
+
// ── File transfer ────────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Wait for the next FileChunk control message on this channel.
|
|
543
|
+
* Used by WshClient.upload/download for 'file'-kind sessions — file
|
|
544
|
+
* transfer moves bytes as ordinary FileChunk control messages rather
|
|
545
|
+
* than through write()/read(), so it works the same whether the
|
|
546
|
+
* channel's data plane is stream- or virtual-backed.
|
|
547
|
+
*
|
|
548
|
+
* Returns `null` if the session closes before another chunk arrives
|
|
549
|
+
* (a truncated transfer, distinct from a chunk with is_final=true).
|
|
550
|
+
*
|
|
551
|
+
* @returns {Promise<object|null>}
|
|
552
|
+
* @internal
|
|
553
|
+
*/
|
|
554
|
+
async _readFileChunk() {
|
|
555
|
+
if (this.#fileChunkQueue.length > 0) {
|
|
556
|
+
return this.#fileChunkQueue.shift();
|
|
557
|
+
}
|
|
558
|
+
if (this.#state === STATE_CLOSED) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
this.#fileChunkWaiter = { resolve, reject };
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Resolve (or clear) any pending _readFileChunk() waiter.
|
|
568
|
+
* @private
|
|
569
|
+
*/
|
|
570
|
+
#resolvePendingFileChunk(value) {
|
|
571
|
+
if (this.#fileChunkWaiter) {
|
|
572
|
+
const { resolve } = this.#fileChunkWaiter;
|
|
573
|
+
this.#fileChunkWaiter = null;
|
|
574
|
+
resolve(value);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
472
578
|
// ── Data stream pump ────────────────────────────────────────────────
|
|
473
579
|
|
|
474
580
|
/**
|