claude-code-session-manager 0.35.17 → 0.35.18
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/dist/assets/{TiptapBody-BM9Kz1Nm.js → TiptapBody-DgFO_m3g.js} +1 -1
- package/dist/assets/{index-DuoC6oCy.js → index-4dJkn9nT.js} +4 -4
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/pty-write-result.test.cjs +46 -0
- package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +181 -0
- package/src/main/pty.cjs +5 -5
- package/src/main/webRemote.cjs +57 -6
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-4dJkn9nT.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="./assets/index-DX2w2YhJ.css">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.18",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pty-write-result.test.cjs — unit tests for PtyManager.write()'s return contract.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/pty-write-result.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'vitest';
|
|
10
|
+
const { manager } = require('../pty.cjs');
|
|
11
|
+
|
|
12
|
+
test('write to an existing session succeeds', () => {
|
|
13
|
+
const tabId = 'tab-existing';
|
|
14
|
+
const proc = { write: () => {} };
|
|
15
|
+
manager.sessions.set(tabId, { proc, cwd: '/tmp', created: 0 });
|
|
16
|
+
try {
|
|
17
|
+
const result = manager.write({ tabId, data: 'hello' });
|
|
18
|
+
expect(result).toEqual({ ok: true });
|
|
19
|
+
} finally {
|
|
20
|
+
manager.sessions.delete(tabId);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('write to a nonexistent tabId returns no-pty', () => {
|
|
25
|
+
const result = manager.write({ tabId: 'tab-does-not-exist', data: 'hello' });
|
|
26
|
+
expect(result).toEqual({ ok: false, reason: 'no-pty' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('write where proc.write throws returns the error reason without throwing', () => {
|
|
30
|
+
const tabId = 'tab-throws';
|
|
31
|
+
const proc = {
|
|
32
|
+
write: () => {
|
|
33
|
+
throw new Error('EPIPE: write after end');
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
manager.sessions.set(tabId, { proc, cwd: '/tmp', created: 0 });
|
|
37
|
+
try {
|
|
38
|
+
let result;
|
|
39
|
+
expect(() => {
|
|
40
|
+
result = manager.write({ tabId, data: 'hello' });
|
|
41
|
+
}).not.toThrow();
|
|
42
|
+
expect(result).toEqual({ ok: false, reason: 'EPIPE: write after end' });
|
|
43
|
+
} finally {
|
|
44
|
+
manager.sessions.delete(tabId);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* web-remote-e2e-pinning.test.cjs — TOFU pinning for the web-remote E2E handshake.
|
|
5
|
+
*
|
|
6
|
+
* Once a browser's SPKI pubkey is manually SAS-confirmed for a paired device,
|
|
7
|
+
* a later `e2e:hello` presenting the SAME pubkey for that device should
|
|
8
|
+
* auto-authenticate (no `confirm-sas` needed); a DIFFERENT pubkey must still
|
|
9
|
+
* fall through to the manual `pending_sas` flow.
|
|
10
|
+
*
|
|
11
|
+
* Runs against a tmp HOME (config.cjs / webRemote.cjs both resolve paths off
|
|
12
|
+
* os.homedir()) and stubs the `electron` module in require.cache, mirroring
|
|
13
|
+
* the tmp-HOME + require.cache-stub pattern used by exchanges.test.cjs.
|
|
14
|
+
*
|
|
15
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/web-remote-e2e-pinning.test.cjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs')
|
|
21
|
+
const fsp = require('node:fs/promises')
|
|
22
|
+
const path = require('node:path')
|
|
23
|
+
const os = require('node:os')
|
|
24
|
+
const crypto = require('node:crypto')
|
|
25
|
+
|
|
26
|
+
/** Generate a P-256 keypair in the same encoding webRemote.cjs uses (SPKI/PKCS8, base64url). */
|
|
27
|
+
function genP256KeyPairB64() {
|
|
28
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
|
|
29
|
+
namedCurve: 'P-256',
|
|
30
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
31
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
32
|
+
})
|
|
33
|
+
return {
|
|
34
|
+
priv: privateKey.toString('base64url'),
|
|
35
|
+
pub: publicKey.toString('base64url'),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let tmpHome
|
|
40
|
+
let origHome
|
|
41
|
+
let webRemote
|
|
42
|
+
let configPath
|
|
43
|
+
let handlers // captured ipcMain.handle(name, fn) map
|
|
44
|
+
|
|
45
|
+
function freshRequire(mod) {
|
|
46
|
+
delete require.cache[require.resolve(mod)]
|
|
47
|
+
return require(mod)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readConfig() {
|
|
51
|
+
const text = await fsp.readFile(configPath, 'utf8')
|
|
52
|
+
return JSON.parse(text)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function writeConfig(cfg) {
|
|
56
|
+
await fsp.mkdir(path.dirname(configPath), { recursive: true })
|
|
57
|
+
await fsp.writeFile(configPath, JSON.stringify(cfg, null, 2) + '\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
tmpHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'web-remote-e2e-pinning-'))
|
|
62
|
+
origHome = process.env.HOME
|
|
63
|
+
process.env.HOME = tmpHome
|
|
64
|
+
|
|
65
|
+
// Stub 'electron' before webRemote.cjs (and logs.cjs, transitively) require it —
|
|
66
|
+
// outside a real Electron process, require('electron') just resolves to a path
|
|
67
|
+
// string, so destructuring { ipcMain, app } off it would throw at module load.
|
|
68
|
+
handlers = new Map()
|
|
69
|
+
const electronPath = require.resolve('electron')
|
|
70
|
+
require.cache[electronPath] = {
|
|
71
|
+
id: electronPath,
|
|
72
|
+
filename: electronPath,
|
|
73
|
+
loaded: true,
|
|
74
|
+
exports: {
|
|
75
|
+
ipcMain: {
|
|
76
|
+
handle: (name, fn) => handlers.set(name, fn),
|
|
77
|
+
on: () => {},
|
|
78
|
+
},
|
|
79
|
+
app: {
|
|
80
|
+
getPath: () => tmpHome,
|
|
81
|
+
getVersion: () => '0.0.0-test',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clear anything already cached under the real HOME so paths re-resolve under tmpHome.
|
|
87
|
+
for (const mod of ['../webRemote.cjs', '../config.cjs', '../logs.cjs', './e2eStateMachine.cjs']) {
|
|
88
|
+
try { delete require.cache[require.resolve(mod)] } catch { /* not yet loaded */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
webRemote = freshRequire('../webRemote.cjs')
|
|
92
|
+
configPath = path.join(tmpHome, '.claude', 'session-manager', 'web-remote.json')
|
|
93
|
+
|
|
94
|
+
webRemote.registerRemoteHandlers()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
process.env.HOME = origHome
|
|
99
|
+
await fsp.rm(tmpHome, { recursive: true, force: true })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('web-remote E2E SAS pinning', () => {
|
|
103
|
+
const deviceKeys = genP256KeyPairB64() // desktop's own E2E keypair
|
|
104
|
+
const browserA = genP256KeyPairB64() // first browser's keypair
|
|
105
|
+
const browserB = genP256KeyPairB64() // a different browser's keypair
|
|
106
|
+
const deviceId = 'device-under-test'
|
|
107
|
+
|
|
108
|
+
function baseDeviceRow(overrides = {}) {
|
|
109
|
+
return {
|
|
110
|
+
deviceId,
|
|
111
|
+
deviceToken: 'tok-abc',
|
|
112
|
+
e2ePrivateKey: deviceKeys.priv,
|
|
113
|
+
e2ePublicKey: deviceKeys.pub,
|
|
114
|
+
deviceName: 'Test Device',
|
|
115
|
+
issuedAt: new Date(0).toISOString(),
|
|
116
|
+
lastConnectedAt: null,
|
|
117
|
+
verifiedPeerPubKey: null,
|
|
118
|
+
...overrides,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
it('1) first e2e:hello for a never-confirmed device lands in pending_sas', async () => {
|
|
123
|
+
await writeConfig({ remoteEnabled: true, remoteControlEnabled: false, devices: [baseDeviceRow()] })
|
|
124
|
+
|
|
125
|
+
const cfg = await readConfig()
|
|
126
|
+
const device = cfg.devices[0]
|
|
127
|
+
await webRemote._internal.handleMessage(
|
|
128
|
+
JSON.stringify({ type: 'e2e:hello', id: 'm1', payload: { pubKey: browserA.pub } }),
|
|
129
|
+
device
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
expect(webRemote._internal.getE2eState().state).toBe('pending_sas')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('2) a manual confirm-sas persists the connected device\'s verifiedPeerPubKey', async () => {
|
|
136
|
+
const confirmSas = handlers.get('webRemote:confirm-sas')
|
|
137
|
+
expect(typeof confirmSas).toBe('function')
|
|
138
|
+
|
|
139
|
+
const result = await confirmSas()
|
|
140
|
+
expect(result.ok).toBe(true)
|
|
141
|
+
expect(webRemote._internal.getE2eState().state).toBe('authenticated')
|
|
142
|
+
|
|
143
|
+
const cfg = await readConfig()
|
|
144
|
+
const persisted = cfg.devices.find((d) => d.deviceId === deviceId)
|
|
145
|
+
expect(persisted.verifiedPeerPubKey).toBe(browserA.pub)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('3) a second e2e:hello presenting the SAME browserPubKey auto-authenticates (no confirm-sas)', async () => {
|
|
149
|
+
// Simulate a fresh reconnect: new WS session resets E2E state, and the device
|
|
150
|
+
// row is reloaded from disk (as connect() would), now carrying the pinned key.
|
|
151
|
+
webRemote._internal.resetE2e()
|
|
152
|
+
expect(webRemote._internal.getE2eState().state).toBe('idle')
|
|
153
|
+
|
|
154
|
+
const cfg = await readConfig()
|
|
155
|
+
const device = cfg.devices.find((d) => d.deviceId === deviceId)
|
|
156
|
+
expect(device.verifiedPeerPubKey).toBe(browserA.pub)
|
|
157
|
+
|
|
158
|
+
await webRemote._internal.handleMessage(
|
|
159
|
+
JSON.stringify({ type: 'e2e:hello', id: 'm2', payload: { pubKey: browserA.pub } }),
|
|
160
|
+
device
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
expect(webRemote._internal.getE2eState().state).toBe('authenticated')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('4) a e2e:hello presenting a DIFFERENT browserPubKey still lands in pending_sas', async () => {
|
|
167
|
+
webRemote._internal.resetE2e()
|
|
168
|
+
expect(webRemote._internal.getE2eState().state).toBe('idle')
|
|
169
|
+
|
|
170
|
+
const cfg = await readConfig()
|
|
171
|
+
const device = cfg.devices.find((d) => d.deviceId === deviceId)
|
|
172
|
+
expect(device.verifiedPeerPubKey).toBe(browserA.pub) // still pinned to A, not B
|
|
173
|
+
|
|
174
|
+
await webRemote._internal.handleMessage(
|
|
175
|
+
JSON.stringify({ type: 'e2e:hello', id: 'm3', payload: { pubKey: browserB.pub } }),
|
|
176
|
+
device
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(webRemote._internal.getE2eState().state).toBe('pending_sas')
|
|
180
|
+
})
|
|
181
|
+
})
|
package/src/main/pty.cjs
CHANGED
|
@@ -188,19 +188,19 @@ class PtyManager {
|
|
|
188
188
|
// Tab was removed or never existed — tell the renderer so it can surface
|
|
189
189
|
// "skipped" feedback rather than silently dropping the write.
|
|
190
190
|
sendIfAlive(this.window, 'pty:write-error', { tabId, reason: 'no-pty' });
|
|
191
|
-
return;
|
|
191
|
+
return { ok: false, reason: 'no-pty' };
|
|
192
192
|
}
|
|
193
193
|
try {
|
|
194
194
|
s.proc.write(data);
|
|
195
|
+
return { ok: true };
|
|
195
196
|
} catch (err) {
|
|
196
197
|
// node-pty throws synchronously (or the underlying net.Socket emits an
|
|
197
198
|
// error that node-pty re-throws) when writing to an exited process.
|
|
198
199
|
// Catch here so the uncaught-exception handler never sees it, and notify
|
|
199
200
|
// the renderer to surface "skipped" feedback.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
201
|
+
const reason = String(err?.message || 'write-failed');
|
|
202
|
+
sendIfAlive(this.window, 'pty:write-error', { tabId, reason });
|
|
203
|
+
return { ok: false, reason };
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
package/src/main/webRemote.cjs
CHANGED
|
@@ -189,6 +189,11 @@ let _destroyed = false; // set at app shutdown to stop reconnect loops
|
|
|
189
189
|
// E2E session state — reset on each new WS connection.
|
|
190
190
|
// .state: 'idle' | 'pending_sas' | 'authenticated' | 'failed'
|
|
191
191
|
let _e2e = makeState();
|
|
192
|
+
// Browser SPKI pubkey (base64url) that produced the current _e2e session, and the
|
|
193
|
+
// deviceId it was received on — tracked so a manual SAS confirmation can pin it to
|
|
194
|
+
// that device's `verifiedPeerPubKey` for auto-trust on future reconnects.
|
|
195
|
+
let _e2ePeerPubKey = null;
|
|
196
|
+
let _e2eDeviceId = null;
|
|
192
197
|
|
|
193
198
|
// ─── Config helpers ───────────────────────────────────────────────────────────
|
|
194
199
|
|
|
@@ -317,6 +322,8 @@ async function getDeviceTicket(deviceToken) {
|
|
|
317
322
|
|
|
318
323
|
function resetE2e(state = 'idle') {
|
|
319
324
|
_e2e = makeState(state);
|
|
325
|
+
_e2ePeerPubKey = null;
|
|
326
|
+
_e2eDeviceId = null;
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
// ─── WebSocket lifecycle ──────────────────────────────────────────────────────
|
|
@@ -781,6 +788,19 @@ async function maybeSummarize(w) {
|
|
|
781
788
|
});
|
|
782
789
|
}
|
|
783
790
|
|
|
791
|
+
/**
|
|
792
|
+
* Shared finish-up for any path that transitions _e2e into 'authenticated'
|
|
793
|
+
* (manual SAS confirmation or pinned-key auto-trust): log, broadcast the new
|
|
794
|
+
* status, and flush the session list immediately rather than waiting for the
|
|
795
|
+
* next SESSION_LIST_PUSH_MS tick.
|
|
796
|
+
*/
|
|
797
|
+
function finalizeE2eAuthenticated(logMessage) {
|
|
798
|
+
logs.writeLine({ scope: 'webRemote', level: 'info', message: logMessage });
|
|
799
|
+
broadcastStatus();
|
|
800
|
+
_lastSessionListJson = null;
|
|
801
|
+
pushSessionList().catch(() => {});
|
|
802
|
+
}
|
|
803
|
+
|
|
784
804
|
// ─── Message handling & command dispatch ─────────────────────────────────────
|
|
785
805
|
|
|
786
806
|
async function handleMessage(raw, device) {
|
|
@@ -883,6 +903,18 @@ async function handleMessage(raw, device) {
|
|
|
883
903
|
broadcastStatus();
|
|
884
904
|
return;
|
|
885
905
|
}
|
|
906
|
+
_e2ePeerPubKey = browserPubKey;
|
|
907
|
+
_e2eDeviceId = device.deviceId;
|
|
908
|
+
// TOFU pinning: if this exact (deviceId, browserPubKey) pair was already
|
|
909
|
+
// manually SAS-confirmed once, skip pending_sas and auto-authenticate — the
|
|
910
|
+
// user already verified this browser. A different/first-seen pubKey always
|
|
911
|
+
// falls through to the manual pending_sas flow below, unchanged.
|
|
912
|
+
if (device.verifiedPeerPubKey && device.verifiedPeerPubKey === browserPubKey) {
|
|
913
|
+
_e2e = makeState('authenticated', sessionKey, null);
|
|
914
|
+
finalizeE2eAuthenticated('E2E session auto-authenticated — pinned browser key matched');
|
|
915
|
+
respond(id, undefined, 'e2e:ready');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
886
918
|
_e2e = makeState('pending_sas', sessionKey, pendingSas);
|
|
887
919
|
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established — SAS pending confirmation' });
|
|
888
920
|
broadcastStatus();
|
|
@@ -1047,8 +1079,7 @@ function getDispatchMap() {
|
|
|
1047
1079
|
|
|
1048
1080
|
'cmd:pty:write': async (payload) => {
|
|
1049
1081
|
const parsed = schemas.ptyWrite.parse(payload);
|
|
1050
|
-
ptyManager.write(parsed);
|
|
1051
|
-
return { ok: true };
|
|
1082
|
+
return ptyManager.write(parsed);
|
|
1052
1083
|
},
|
|
1053
1084
|
|
|
1054
1085
|
'cmd:pty:resize': async (payload) => {
|
|
@@ -1167,6 +1198,9 @@ async function pair(otp) {
|
|
|
1167
1198
|
deviceName: `Device (paired ${new Date().toISOString().slice(0, 10)})`,
|
|
1168
1199
|
issuedAt: new Date().toISOString(),
|
|
1169
1200
|
lastConnectedAt: null,
|
|
1201
|
+
// Set once a browser's SAS is manually confirmed; TOFU-pinned for auto-trust
|
|
1202
|
+
// on future reconnects from the same (deviceId, browserPubKey) pair.
|
|
1203
|
+
verifiedPeerPubKey: null,
|
|
1170
1204
|
}];
|
|
1171
1205
|
|
|
1172
1206
|
await saveConfig({ ...cfg, devices });
|
|
@@ -1234,13 +1268,23 @@ function registerRemoteHandlers() {
|
|
|
1234
1268
|
return { ok: false, error };
|
|
1235
1269
|
}
|
|
1236
1270
|
_e2e = next;
|
|
1237
|
-
|
|
1238
|
-
|
|
1271
|
+
// Pin this browser's pubkey to the device so future reconnects with the same
|
|
1272
|
+
// key auto-authenticate (see e2e:hello above) instead of re-prompting forever.
|
|
1273
|
+
if (_e2ePeerPubKey && _e2eDeviceId) {
|
|
1274
|
+
try {
|
|
1275
|
+
const cfg = await loadConfig();
|
|
1276
|
+
const devices = (cfg.devices || []).map((d) =>
|
|
1277
|
+
d.deviceId === _e2eDeviceId ? { ...d, verifiedPeerPubKey: _e2ePeerPubKey } : d
|
|
1278
|
+
);
|
|
1279
|
+
await saveConfig({ ...cfg, devices });
|
|
1280
|
+
} catch (e) {
|
|
1281
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'failed to persist pinned peer pubkey', meta: { error: e?.message } });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1239
1284
|
// Flush the session list immediately — the push loop was suppressed while
|
|
1240
1285
|
// state !== 'authenticated', so the mobile app would otherwise wait up to
|
|
1241
1286
|
// SESSION_LIST_PUSH_MS for the first useful data.
|
|
1242
|
-
|
|
1243
|
-
pushSessionList().catch(() => {});
|
|
1287
|
+
finalizeE2eAuthenticated('E2E session authenticated — SAS confirmed by user');
|
|
1244
1288
|
return { ok: true };
|
|
1245
1289
|
});
|
|
1246
1290
|
|
|
@@ -1334,4 +1378,11 @@ module.exports = {
|
|
|
1334
1378
|
registerRemoteHandlers,
|
|
1335
1379
|
init,
|
|
1336
1380
|
destroy,
|
|
1381
|
+
// Test-only internals — not part of the IPC surface. Lets unit tests drive
|
|
1382
|
+
// e2e:hello / SAS-pinning transitions without a real relay WS connection.
|
|
1383
|
+
_internal: {
|
|
1384
|
+
handleMessage,
|
|
1385
|
+
resetE2e,
|
|
1386
|
+
getE2eState: () => _e2e,
|
|
1387
|
+
},
|
|
1337
1388
|
};
|