@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/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(transport, channelId, streamIds, kind, { dataMode = 'stream', capabilities = [] } = {}) {
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
  /**