@johnhenry/wsh 0.13.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 +1 -1
- package/src/client.mjs +86 -43
- package/src/index.d.ts +46 -2
- package/src/messages.gen.mjs +7 -9
- package/src/session.mjs +45 -1
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -134,8 +134,18 @@ export class WshClient {
|
|
|
134
134
|
/** @type {string|null} Session ID assigned by the server after authentication. */
|
|
135
135
|
#sessionId = null;
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* @type {string|null} Connection-level token from AUTH_OK, bound to
|
|
139
|
+
* this connection's own auth session_id (from CHALLENGE/SERVER_HELLO) --
|
|
140
|
+
* NOT a PTY/exec session's token. Renamed from `#resumeToken` (clawser
|
|
141
|
+
* #48): the old name invited exactly the bug this fix addresses --
|
|
142
|
+
* attachSession() used to send this as if it were a valid token for the
|
|
143
|
+
* *target* session_id, which it can never be (it's scoped to a
|
|
144
|
+
* different session_id entirely). Not used for anything else currently;
|
|
145
|
+
* kept and exposed via `authToken` for potential future connection-level
|
|
146
|
+
* resume use, mirroring the Rust client's `WshClient::token()`.
|
|
147
|
+
*/
|
|
148
|
+
#authToken = null;
|
|
139
149
|
|
|
140
150
|
/** @type {import('./transport.mjs').WshTransport|null} Active transport. */
|
|
141
151
|
#transport = null;
|
|
@@ -282,6 +292,16 @@ export class WshClient {
|
|
|
282
292
|
return this.#sessionId;
|
|
283
293
|
}
|
|
284
294
|
|
|
295
|
+
/**
|
|
296
|
+
* This connection's own AUTH-level token (from AUTH_OK), scoped to this
|
|
297
|
+
* connection's auth session_id -- NOT a PTY/exec session token. See
|
|
298
|
+
* `WshSession.resumeToken` for the per-session credential used by
|
|
299
|
+
* `resumeSession`.
|
|
300
|
+
*/
|
|
301
|
+
get authToken() {
|
|
302
|
+
return this.#authToken;
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
/** Read-only view of active sessions. */
|
|
286
306
|
get sessions() {
|
|
287
307
|
return new Map(this.#sessions);
|
|
@@ -341,7 +361,7 @@ export class WshClient {
|
|
|
341
361
|
this.#channelCounter = 0;
|
|
342
362
|
this.#waiters.clear();
|
|
343
363
|
this.#sessionId = null;
|
|
344
|
-
this.#
|
|
364
|
+
this.#authToken = null;
|
|
345
365
|
|
|
346
366
|
try {
|
|
347
367
|
// ── Select and connect transport ──────────────────────────────
|
|
@@ -384,7 +404,7 @@ export class WshClient {
|
|
|
384
404
|
if (challengeMsg.type === MSG.AUTH_OK) {
|
|
385
405
|
// Server accepted without challenge (e.g. trusted key).
|
|
386
406
|
this.#sessionId = challengeMsg.session_id || tempSessionId;
|
|
387
|
-
this.#
|
|
407
|
+
this.#authToken = challengeMsg.token || null;
|
|
388
408
|
this.#state = STATE_AUTHENTICATED;
|
|
389
409
|
this.#startPing();
|
|
390
410
|
return this.#sessionId;
|
|
@@ -460,7 +480,7 @@ export class WshClient {
|
|
|
460
480
|
}
|
|
461
481
|
|
|
462
482
|
this.#sessionId = authResult.session_id || tempSessionId;
|
|
463
|
-
this.#
|
|
483
|
+
this.#authToken = authResult.token || null;
|
|
464
484
|
this.#state = STATE_AUTHENTICATED;
|
|
465
485
|
this.#startPing();
|
|
466
486
|
|
|
@@ -538,37 +558,48 @@ export class WshClient {
|
|
|
538
558
|
/**
|
|
539
559
|
* Attach to an existing remote session (collaborative or read-only).
|
|
540
560
|
*
|
|
561
|
+
* Authorization is satisfied by EITHER `token` OR the caller already
|
|
562
|
+
* owning/being ACL-granted access to `targetSessionId` server-side (see
|
|
563
|
+
* wsh-server's `check_session_access`) -- `token` is optional and, for
|
|
564
|
+
* the common case of attaching via ownership or a `grantSessionAccess`
|
|
565
|
+
* grant, should simply be omitted (a granted principal never receives
|
|
566
|
+
* the session's token to begin with -- only the opener does, via
|
|
567
|
+
* `WshSession.resumeToken`). Pass one explicitly only if you have it
|
|
568
|
+
* (e.g. this same process opened the session earlier, or the owner
|
|
569
|
+
* shared it with you out of band).
|
|
570
|
+
*
|
|
571
|
+
* Previously (clawser #48) this always sent this connection's own
|
|
572
|
+
* AUTH-level token, which can never verify against a *different*
|
|
573
|
+
* session_id -- making attachSession() unreachable for everyone,
|
|
574
|
+
* including the session's own owner from a fresh connection. Also fixed
|
|
575
|
+
* alongside that: the server actually replies with PRESENCE (success)
|
|
576
|
+
* or ERROR (failure), not OPEN_OK/OPEN_FAIL -- this used to wait on the
|
|
577
|
+
* wrong response stream entirely and would have hung even once the
|
|
578
|
+
* token bug was fixed.
|
|
579
|
+
*
|
|
541
580
|
* @param {string} targetSessionId - Remote session ID to attach to
|
|
542
581
|
* @param {object} [opts]
|
|
543
582
|
* @param {boolean} [opts.readOnly=false] - Attach in read-only mode
|
|
583
|
+
* @param {Uint8Array} [opts.token] - Session-scoped token (optional; see above)
|
|
544
584
|
* @param {number} [opts.timeout] - Timeout in ms
|
|
545
|
-
* @returns {Promise<object>} Server's response
|
|
585
|
+
* @returns {Promise<object>} Server's PRESENCE response
|
|
546
586
|
*/
|
|
547
|
-
async attachSession(targetSessionId, { readOnly = false, timeout = DEFAULT_OPEN_TIMEOUT } = {}) {
|
|
587
|
+
async attachSession(targetSessionId, { readOnly = false, token, timeout = DEFAULT_OPEN_TIMEOUT } = {}) {
|
|
548
588
|
this.#assertAuthenticated('attachSession');
|
|
549
589
|
|
|
550
590
|
const attachMode = readOnly ? 'readonly' : 'control';
|
|
551
591
|
await this.#transport.sendControl(
|
|
552
|
-
attachMsg({ sessionId: targetSessionId, token
|
|
592
|
+
attachMsg({ sessionId: targetSessionId, token, mode: attachMode })
|
|
553
593
|
);
|
|
554
594
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const response = await new Promise((resolve, reject) => {
|
|
561
|
-
const entry = { mode: 'raw', resolve, reject };
|
|
562
|
-
entry.timer = setTimeout(() => {
|
|
563
|
-
const idx = this.#pendingOpens.indexOf(entry);
|
|
564
|
-
if (idx !== -1) this.#pendingOpens.splice(idx, 1);
|
|
565
|
-
reject(new Error('Timed out waiting for attach response'));
|
|
566
|
-
}, timeout);
|
|
567
|
-
this.#pendingOpens.push(entry);
|
|
568
|
-
});
|
|
595
|
+
const response = await this.#waitForMessage(
|
|
596
|
+
[MSG.PRESENCE, MSG.ERROR],
|
|
597
|
+
timeout,
|
|
598
|
+
'Timed out waiting for attach response'
|
|
599
|
+
);
|
|
569
600
|
|
|
570
|
-
if (response.type === MSG.
|
|
571
|
-
throw new Error(`Failed to attach: ${response.
|
|
601
|
+
if (response.type === MSG.ERROR) {
|
|
602
|
+
throw new Error(`Failed to attach: ${response.message || 'rejected'}`);
|
|
572
603
|
}
|
|
573
604
|
|
|
574
605
|
return response;
|
|
@@ -577,26 +608,35 @@ export class WshClient {
|
|
|
577
608
|
/**
|
|
578
609
|
* Resume a previously disconnected session.
|
|
579
610
|
*
|
|
611
|
+
* Unlike attachSession(), `token` is required and verified
|
|
612
|
+
* unconditionally server-side -- Resume is specifically for the
|
|
613
|
+
* connection that was handed this exact token (via
|
|
614
|
+
* `WshSession.resumeToken`, when it originally opened the session)
|
|
615
|
+
* coming back. A principal who only has ACL/ownership access but never
|
|
616
|
+
* held the token should use attachSession() instead.
|
|
617
|
+
*
|
|
580
618
|
* @param {string} targetSessionId - Session ID to resume
|
|
581
|
-
* @param {
|
|
582
|
-
* @param {
|
|
583
|
-
* @
|
|
619
|
+
* @param {Uint8Array} token - Session-scoped resume token (see `WshSession.resumeToken`)
|
|
620
|
+
* @param {object} [opts]
|
|
621
|
+
* @param {number} [opts.lastSeq=0] - Last sequence number this client already has (currently advisory -- the ring buffer replays its full contents regardless)
|
|
622
|
+
* @param {number} [opts.timeout=10000] - Timeout in ms
|
|
623
|
+
* @returns {Promise<object>} Server's PRESENCE response
|
|
584
624
|
*/
|
|
585
|
-
async resumeSession(targetSessionId, token, { timeout = DEFAULT_OPEN_TIMEOUT } = {}) {
|
|
625
|
+
async resumeSession(targetSessionId, token, { lastSeq = 0, timeout = DEFAULT_OPEN_TIMEOUT } = {}) {
|
|
586
626
|
this.#assertAuthenticated('resumeSession');
|
|
587
627
|
|
|
588
628
|
await this.#transport.sendControl(
|
|
589
|
-
resumeMsg({ sessionId: targetSessionId, token })
|
|
629
|
+
resumeMsg({ sessionId: targetSessionId, token, lastSeq })
|
|
590
630
|
);
|
|
591
631
|
|
|
592
632
|
const response = await this.#waitForMessage(
|
|
593
|
-
[MSG.
|
|
633
|
+
[MSG.PRESENCE, MSG.ERROR],
|
|
594
634
|
timeout,
|
|
595
635
|
'Timed out waiting for resume response'
|
|
596
636
|
);
|
|
597
637
|
|
|
598
|
-
if (response.type === MSG.
|
|
599
|
-
throw new Error(`Failed to resume: ${response.
|
|
638
|
+
if (response.type === MSG.ERROR) {
|
|
639
|
+
throw new Error(`Failed to resume: ${response.message || 'rejected'}`);
|
|
600
640
|
}
|
|
601
641
|
|
|
602
642
|
return response;
|
|
@@ -1667,7 +1707,7 @@ export class WshClient {
|
|
|
1667
1707
|
|
|
1668
1708
|
if (challengeMsg.type === MSG.AUTH_OK) {
|
|
1669
1709
|
this.#sessionId = challengeMsg.session_id || tempSessionId;
|
|
1670
|
-
this.#
|
|
1710
|
+
this.#authToken = challengeMsg.token || null;
|
|
1671
1711
|
this.#state = STATE_AUTHENTICATED;
|
|
1672
1712
|
this.#startPing();
|
|
1673
1713
|
return this.#sessionId;
|
|
@@ -1713,7 +1753,7 @@ export class WshClient {
|
|
|
1713
1753
|
}
|
|
1714
1754
|
|
|
1715
1755
|
this.#sessionId = authResult.session_id || tempSessionId;
|
|
1716
|
-
this.#
|
|
1756
|
+
this.#authToken = authResult.token || null;
|
|
1717
1757
|
this.#state = STATE_AUTHENTICATED;
|
|
1718
1758
|
this.#startPing();
|
|
1719
1759
|
|
|
@@ -1773,14 +1813,6 @@ export class WshClient {
|
|
|
1773
1813
|
if (!pending) return;
|
|
1774
1814
|
clearTimeout(pending.timer);
|
|
1775
1815
|
|
|
1776
|
-
// attachSession() wants the raw response either way and never
|
|
1777
|
-
// constructs a session -- it has no #sessions-registration race to
|
|
1778
|
-
// protect against.
|
|
1779
|
-
if (pending.mode === 'raw') {
|
|
1780
|
-
pending.resolve(msg);
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
1816
|
if (type === MSG.OPEN_FAIL) {
|
|
1785
1817
|
pending.reject(new Error(`Failed to open session: ${msg.reason || 'rejected'}`));
|
|
1786
1818
|
return;
|
|
@@ -1796,7 +1828,18 @@ export class WshClient {
|
|
|
1796
1828
|
serverChannelId,
|
|
1797
1829
|
streamIds,
|
|
1798
1830
|
pending.kind,
|
|
1799
|
-
{
|
|
1831
|
+
{
|
|
1832
|
+
dataMode,
|
|
1833
|
+
capabilities,
|
|
1834
|
+
// clawser #48: OpenOk now carries the session_id/token this
|
|
1835
|
+
// channel belongs to (pty/exec only -- undefined for e.g. file
|
|
1836
|
+
// channels, which have no Attach/Resume-able session). Exposed
|
|
1837
|
+
// via WshSession.sessionId/resumeToken for a later
|
|
1838
|
+
// attachSession()/resumeSession() call, possibly from a
|
|
1839
|
+
// different connection.
|
|
1840
|
+
sessionId: msg.session_id,
|
|
1841
|
+
resumeToken: msg.token,
|
|
1842
|
+
}
|
|
1800
1843
|
);
|
|
1801
1844
|
this.#sessions.set(serverChannelId, session);
|
|
1802
1845
|
|
package/src/index.d.ts
CHANGED
|
@@ -833,6 +833,21 @@ export class WshSession {
|
|
|
833
833
|
/** Session capabilities advertised by OPEN_OK. */
|
|
834
834
|
readonly capabilities: string[];
|
|
835
835
|
|
|
836
|
+
/**
|
|
837
|
+
* The server-assigned session_id this channel belongs to, from OPEN_OK
|
|
838
|
+
* (clawser #48). `undefined` for channel kinds with no Attach/Resume-able
|
|
839
|
+
* session (e.g. file channels). Pass to `WshClient.attachSession`/
|
|
840
|
+
* `resumeSession` from another connection.
|
|
841
|
+
*/
|
|
842
|
+
readonly sessionId: string | undefined;
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The session-scoped resume token minted at Open time, if any (clawser
|
|
846
|
+
* #48). Pass to `WshClient.resumeSession` from a later connection to
|
|
847
|
+
* reclaim this exact session.
|
|
848
|
+
*/
|
|
849
|
+
readonly resumeToken: Uint8Array | undefined;
|
|
850
|
+
|
|
836
851
|
/** Called when stdout/stderr data arrives. */
|
|
837
852
|
onData: ((data: Uint8Array) => void) | null;
|
|
838
853
|
|
|
@@ -874,6 +889,8 @@ export class WshSession {
|
|
|
874
889
|
opts?: {
|
|
875
890
|
dataMode?: 'stream' | 'virtual';
|
|
876
891
|
capabilities?: string[];
|
|
892
|
+
sessionId?: string;
|
|
893
|
+
resumeToken?: Uint8Array;
|
|
877
894
|
},
|
|
878
895
|
);
|
|
879
896
|
|
|
@@ -958,6 +975,21 @@ export interface WshOpenSessionOptions {
|
|
|
958
975
|
/** Options for WshClient.attachSession(). */
|
|
959
976
|
export interface WshAttachSessionOptions {
|
|
960
977
|
readOnly?: boolean;
|
|
978
|
+
/**
|
|
979
|
+
* Session-scoped token (clawser #48; optional). Omit for the common
|
|
980
|
+
* case of attaching via ownership or a `grantSessionAccess` grant --
|
|
981
|
+
* the server accepts EITHER a valid token OR the caller already
|
|
982
|
+
* owning/being ACL-granted access. Pass one (e.g. from
|
|
983
|
+
* `WshSession.resumeToken`) only if you have it.
|
|
984
|
+
*/
|
|
985
|
+
token?: Uint8Array;
|
|
986
|
+
timeout?: number;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Options for WshClient.resumeSession(). */
|
|
990
|
+
export interface WshResumeSessionOptions {
|
|
991
|
+
/** Last sequence number this client already has (currently advisory). */
|
|
992
|
+
lastSeq?: number;
|
|
961
993
|
timeout?: number;
|
|
962
994
|
}
|
|
963
995
|
|
|
@@ -1060,6 +1092,14 @@ export class WshClient {
|
|
|
1060
1092
|
/** Server-assigned session ID. */
|
|
1061
1093
|
readonly sessionId: string | null;
|
|
1062
1094
|
|
|
1095
|
+
/**
|
|
1096
|
+
* This connection's own AUTH-level token (from AUTH_OK), scoped to this
|
|
1097
|
+
* connection's auth session_id -- NOT a PTY/exec session token. See
|
|
1098
|
+
* `WshSession.resumeToken` for the per-session credential used by
|
|
1099
|
+
* `resumeSession`.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly authToken: Uint8Array | null;
|
|
1102
|
+
|
|
1063
1103
|
/** Read-only view of active sessions. */
|
|
1064
1104
|
readonly sessions: Map<number, WshSession>;
|
|
1065
1105
|
|
|
@@ -1118,13 +1158,17 @@ export class WshClient {
|
|
|
1118
1158
|
|
|
1119
1159
|
/**
|
|
1120
1160
|
* Attach to an existing remote session (collaborative or read-only).
|
|
1161
|
+
* Authorization is satisfied by EITHER `opts.token` OR the caller
|
|
1162
|
+
* already owning/being ACL-granted access to `targetSessionId`
|
|
1163
|
+
* server-side -- see `WshAttachSessionOptions.token`.
|
|
1121
1164
|
*/
|
|
1122
1165
|
attachSession(targetSessionId: string, opts?: WshAttachSessionOptions): Promise<WshMessage>;
|
|
1123
1166
|
|
|
1124
1167
|
/**
|
|
1125
|
-
* Resume a previously disconnected session.
|
|
1168
|
+
* Resume a previously disconnected session. Unlike `attachSession()`,
|
|
1169
|
+
* `token` is required and verified unconditionally server-side.
|
|
1126
1170
|
*/
|
|
1127
|
-
resumeSession(targetSessionId: string, token:
|
|
1171
|
+
resumeSession(targetSessionId: string, token: Uint8Array, opts?: WshResumeSessionOptions): Promise<WshMessage>;
|
|
1128
1172
|
|
|
1129
1173
|
/**
|
|
1130
1174
|
* Detach from a remote session, leaving it running server-side.
|
package/src/messages.gen.mjs
CHANGED
|
@@ -247,14 +247,11 @@ export function open({ kind, command, cols, rows, env } = {}) {
|
|
|
247
247
|
return msg;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
export function openOk({ channelId, streamIds = [], dataMode = "stream", capabilities = [] } = {}) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
data_mode: dataMode,
|
|
256
|
-
capabilities,
|
|
257
|
-
};
|
|
250
|
+
export function openOk({ channelId, streamIds = [], dataMode = "stream", capabilities = [], sessionId, token } = {}) {
|
|
251
|
+
const msg = { type: MSG.OPEN_OK, channel_id: channelId, stream_ids: streamIds, data_mode: dataMode, capabilities };
|
|
252
|
+
if (sessionId !== undefined) msg.session_id = sessionId;
|
|
253
|
+
if (token !== undefined) msg.token = token;
|
|
254
|
+
return msg;
|
|
258
255
|
}
|
|
259
256
|
|
|
260
257
|
export function openFail({ reason } = {}) {
|
|
@@ -327,7 +324,8 @@ export function pong({ id } = {}) {
|
|
|
327
324
|
}
|
|
328
325
|
|
|
329
326
|
export function attach({ sessionId, token, mode = "control", deviceLabel } = {}) {
|
|
330
|
-
const msg = { type: MSG.ATTACH, session_id: sessionId,
|
|
327
|
+
const msg = { type: MSG.ATTACH, session_id: sessionId, mode };
|
|
328
|
+
if (token !== undefined) msg.token = token;
|
|
331
329
|
if (deviceLabel !== undefined) msg.device_label = deviceLabel;
|
|
332
330
|
return msg;
|
|
333
331
|
}
|
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.
|
|
@@ -151,8 +168,13 @@ export class WshSession {
|
|
|
151
168
|
* @param {object} [opts]
|
|
152
169
|
* @param {'stream'|'virtual'} [opts.dataMode='stream']
|
|
153
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)
|
|
154
173
|
*/
|
|
155
|
-
constructor(
|
|
174
|
+
constructor(
|
|
175
|
+
transport, channelId, streamIds, kind,
|
|
176
|
+
{ dataMode = 'stream', capabilities = [], sessionId, resumeToken } = {}
|
|
177
|
+
) {
|
|
156
178
|
this.#transport = transport;
|
|
157
179
|
this.channelId = channelId;
|
|
158
180
|
this.#streamIds = streamIds;
|
|
@@ -160,6 +182,28 @@ export class WshSession {
|
|
|
160
182
|
this.id = String(channelId);
|
|
161
183
|
this.#dataMode = dataMode === 'virtual' ? 'virtual' : 'stream';
|
|
162
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;
|
|
163
207
|
}
|
|
164
208
|
|
|
165
209
|
/** Current session state. */
|