claude-code-session-manager 0.21.1 → 0.21.3
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/bin/cli.cjs +5 -0
- package/dist/assets/{TiptapBody-C46DacIO.js → TiptapBody-PdmsfUCQ.js} +2 -2
- package/dist/assets/cssMode-DfqZGMQs.js +1 -0
- package/dist/assets/{freemarker2-BxIPNQn-.js → freemarker2-XTPYh37h.js} +1 -1
- package/dist/assets/handlebars-DKUF5VyH.js +1 -0
- package/dist/assets/html-uqoqsIeI.js +1 -0
- package/dist/assets/htmlMode-aMTQs1su.js +1 -0
- package/dist/assets/index-DO3ROR11.js +3525 -0
- package/dist/assets/index-DeQI4oVI.css +32 -0
- package/dist/assets/javascript-BVxRZMds.js +1 -0
- package/dist/assets/{jsonMode-1FAJaHiX.js → jsonMode-D04xP2s5.js} +4 -4
- package/dist/assets/liquid-BkQHTH2P.js +1 -0
- package/dist/assets/lspLanguageFeatures-By9uLznH.js +4 -0
- package/dist/assets/mdx-Du1IlbjV.js +1 -0
- package/dist/assets/{index-oGyPFfYZ.css → monaco-editor-BTnBOi8r.css} +1 -32
- package/dist/assets/monaco-editor-BW5C4Iv1.js +908 -0
- package/dist/assets/python-DSlImqXd.js +1 -0
- package/dist/assets/razor-BmUVyvSK.js +1 -0
- package/dist/assets/{tsMode-CLQIVays.js → tsMode-Btj0TTH7.js} +1 -1
- package/dist/assets/typescript-Bzelq9vO.js +1 -0
- package/dist/assets/xml-Whd9EaSd.js +1 -0
- package/dist/assets/yaml-QYf0-IN8.js +1 -0
- package/dist/index.html +4 -2
- package/package.json +1 -1
- package/src/main/__tests__/runVerify.test.cjs +101 -0
- package/src/main/config.cjs +36 -4
- package/src/main/historyAggregator.cjs +400 -149
- package/src/main/index.cjs +8 -0
- package/src/main/ipcSchemas.cjs +42 -13
- package/src/main/kg.cjs +87 -30
- package/src/main/lib/credentials.cjs +7 -0
- package/src/main/lib/e2eStateMachine.cjs +39 -0
- package/src/main/runVerify.cjs +28 -5
- package/src/main/scheduler/prdParser.cjs +16 -1
- package/src/main/scheduler.cjs +97 -13
- package/src/main/transcripts.cjs +141 -19
- package/src/main/usageMatrix.cjs +7 -3
- package/src/main/webRemote.cjs +190 -29
- package/src/preload/api.d.ts +40 -0
- package/src/preload/index.cjs +7 -0
- package/dist/assets/cssMode-CauFS5Bp.js +0 -1
- package/dist/assets/handlebars-DnEVFUsu.js +0 -1
- package/dist/assets/html-S8NXUTqc.js +0 -1
- package/dist/assets/htmlMode-rSEyII9x.js +0 -1
- package/dist/assets/index-DMobTczM.js +0 -4431
- package/dist/assets/javascript-BiWR68QP.js +0 -1
- package/dist/assets/liquid-CEtOkbwI.js +0 -1
- package/dist/assets/lspLanguageFeatures-CRF3U0x3.js +0 -4
- package/dist/assets/mdx-C7C95Bzt.js +0 -1
- package/dist/assets/python-CXvKcjLk.js +0 -1
- package/dist/assets/razor-tzZHfRy2.js +0 -1
- package/dist/assets/typescript-LxhyM9W2.js +0 -1
- package/dist/assets/xml-VS_m20VE.js +0 -1
- package/dist/assets/yaml-BsjggdVD.js +0 -1
package/src/main/webRemote.cjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Outbound-only: opens a WS TO the relay, never listens.
|
|
8
8
|
* - OFF by default: remoteEnabled must be explicitly true in web-remote.json.
|
|
9
9
|
* - Kill switch: synchronous — drops socket + refuses all commands instantly.
|
|
10
|
-
* - Strict allowlist:
|
|
10
|
+
* - Strict allowlist: enumerated command types; unknown → opaque rejected response.
|
|
11
11
|
* - Zod validation: every payload parsed before any dispatch.
|
|
12
12
|
* - Path safety: cwd/path fields go through validatePath (home-dir boundary).
|
|
13
13
|
* - Token at rest: web-remote.json written at 0600 via writeTextAtomic.
|
|
@@ -28,6 +28,7 @@ const { writeTextAtomic, validatePath } = require('./config.cjs');
|
|
|
28
28
|
const logs = require('./logs.cjs');
|
|
29
29
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
30
30
|
const { schemas } = require('./ipcSchemas.cjs');
|
|
31
|
+
const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
|
|
31
32
|
|
|
32
33
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
33
34
|
|
|
@@ -66,7 +67,7 @@ const MSG_MAX_BYTES = 256 * 1024;
|
|
|
66
67
|
|
|
67
68
|
// Single source of truth lives in ipcSchemas.cjs — imported here so the test
|
|
68
69
|
// can verify the same Set without depending on Electron-linked modules.
|
|
69
|
-
const { ALLOWED_COMMANDS } = require('./ipcSchemas.cjs');
|
|
70
|
+
const { ALLOWED_COMMANDS, MUTATE_COMMANDS, SAS_GATED_READS } = require('./ipcSchemas.cjs');
|
|
70
71
|
|
|
71
72
|
// ─── E2E encryption helpers (P-256 ECDH + AES-256-GCM, ADR §5.2) ────────────
|
|
72
73
|
|
|
@@ -111,6 +112,30 @@ function deriveSessionKey(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
|
|
|
111
112
|
return Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 32));
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Derive a 6-digit Short Authentication String from the ECDH shared secret.
|
|
117
|
+
* Uses a separate HKDF info label ('sm-sas-v1') so the SAS is independent of
|
|
118
|
+
* the session key. Both sides compute the same value; user confirms they match.
|
|
119
|
+
*/
|
|
120
|
+
function deriveSas(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
|
|
121
|
+
const myPrivKey = crypto.createPrivateKey({
|
|
122
|
+
key: Buffer.from(myPrivateKeyB64, 'base64url'),
|
|
123
|
+
format: 'der',
|
|
124
|
+
type: 'pkcs8',
|
|
125
|
+
});
|
|
126
|
+
const peerPubKey = crypto.createPublicKey({
|
|
127
|
+
key: Buffer.from(peerPublicKeyB64, 'base64url'),
|
|
128
|
+
format: 'der',
|
|
129
|
+
type: 'spki',
|
|
130
|
+
});
|
|
131
|
+
const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
|
|
132
|
+
const salt = Buffer.from(deviceId, 'utf8');
|
|
133
|
+
const info = Buffer.from('sm-sas-v1', 'utf8');
|
|
134
|
+
const sasBytes = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 3));
|
|
135
|
+
const sasNum = ((sasBytes[0] << 16) | (sasBytes[1] << 8) | sasBytes[2]) % 1_000_000;
|
|
136
|
+
return sasNum.toString().padStart(6, '0');
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
/**
|
|
115
140
|
* Encrypt a plaintext JSON string into an AES-256-GCM box.
|
|
116
141
|
* @returns { nonce: base64url, ciphertext: base64url } — the nonce is 12 random bytes
|
|
@@ -161,12 +186,13 @@ let _configCacheAt = 0;
|
|
|
161
186
|
let _destroyed = false; // set at app shutdown to stop reconnect loops
|
|
162
187
|
|
|
163
188
|
// E2E session state — reset on each new WS connection.
|
|
164
|
-
|
|
189
|
+
// .state: 'idle' | 'pending_sas' | 'authenticated' | 'failed'
|
|
190
|
+
let _e2e = makeState();
|
|
165
191
|
|
|
166
192
|
// ─── Config helpers ───────────────────────────────────────────────────────────
|
|
167
193
|
|
|
168
194
|
function defaultConfig() {
|
|
169
|
-
return { remoteEnabled: false, devices: [] };
|
|
195
|
+
return { remoteEnabled: false, remoteControlEnabled: false, devices: [] };
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
function loadConfigSync() {
|
|
@@ -219,7 +245,8 @@ async function auditLog(ts, type, deviceId, msgId, result) {
|
|
|
219
245
|
try {
|
|
220
246
|
const ymd = ts.slice(0, 10);
|
|
221
247
|
const logPath = path.join(AUDIT_LOG_DIR, `remote-audit-${ymd}.log`);
|
|
222
|
-
const
|
|
248
|
+
const clean = (s) => String(s ?? '').replace(/[\r\n\t]+/g, ' ').slice(0, 200);
|
|
249
|
+
const line = `${ts} ${clean(type)} deviceId=${clean(deviceId) || '-'} msgId=${clean(msgId) || '-'} result=${result}\n`;
|
|
223
250
|
const handle = await fsp.open(logPath, 'a', 0o600);
|
|
224
251
|
try {
|
|
225
252
|
await handle.write(line);
|
|
@@ -285,6 +312,12 @@ async function getDeviceTicket(deviceToken) {
|
|
|
285
312
|
return result.ticket;
|
|
286
313
|
}
|
|
287
314
|
|
|
315
|
+
// ─── E2E state helpers ────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
function resetE2e(state = 'idle') {
|
|
318
|
+
_e2e = makeState(state);
|
|
319
|
+
}
|
|
320
|
+
|
|
288
321
|
// ─── WebSocket lifecycle ──────────────────────────────────────────────────────
|
|
289
322
|
|
|
290
323
|
function broadcastStatus() {
|
|
@@ -293,8 +326,12 @@ function broadcastStatus() {
|
|
|
293
326
|
const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
|
|
294
327
|
sendIfAlive(_window, 'webRemote:status', {
|
|
295
328
|
enabled: cfg.remoteEnabled,
|
|
329
|
+
remoteControlEnabled: cfg.remoteControlEnabled ?? false,
|
|
296
330
|
connected,
|
|
297
|
-
e2eActive: connected &&
|
|
331
|
+
e2eActive: connected && _e2e.sessionKey !== null,
|
|
332
|
+
e2eAuthenticated: connected && _e2e.state === 'authenticated',
|
|
333
|
+
e2eState: connected ? _e2e.state : 'idle',
|
|
334
|
+
pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
|
|
298
335
|
devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
|
|
299
336
|
deviceId, deviceName, issuedAt, lastConnectedAt,
|
|
300
337
|
})),
|
|
@@ -358,7 +395,7 @@ function scheduleReconnect() {
|
|
|
358
395
|
async function disconnect() {
|
|
359
396
|
cancelReconnect();
|
|
360
397
|
stopHeartbeat();
|
|
361
|
-
|
|
398
|
+
resetE2e();
|
|
362
399
|
if (_ws) {
|
|
363
400
|
const ws = _ws;
|
|
364
401
|
_ws = null;
|
|
@@ -407,7 +444,7 @@ async function connect() {
|
|
|
407
444
|
|
|
408
445
|
_ws = ws;
|
|
409
446
|
_missedPongs = 0;
|
|
410
|
-
|
|
447
|
+
resetE2e(); // reset E2E state on new connection
|
|
411
448
|
|
|
412
449
|
ws.on('open', () => {
|
|
413
450
|
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'connected to relay' });
|
|
@@ -426,6 +463,8 @@ async function connect() {
|
|
|
426
463
|
}).catch(() => {});
|
|
427
464
|
broadcastStatus();
|
|
428
465
|
// v2: begin pushing the live session list once connected.
|
|
466
|
+
// Reset diff-guard so the new client always receives a full session-list push.
|
|
467
|
+
_lastSessionListJson = null;
|
|
429
468
|
startSessionListPush();
|
|
430
469
|
});
|
|
431
470
|
|
|
@@ -442,7 +481,7 @@ async function connect() {
|
|
|
442
481
|
ws.on('close', (code) => {
|
|
443
482
|
stopHeartbeat();
|
|
444
483
|
stopAllSessionWatches();
|
|
445
|
-
|
|
484
|
+
resetE2e();
|
|
446
485
|
if (_ws === ws) _ws = null;
|
|
447
486
|
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'ws closed', meta: { code } });
|
|
448
487
|
broadcastStatus();
|
|
@@ -483,14 +522,15 @@ const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
|
|
|
483
522
|
|
|
484
523
|
const _sessionWatchers = new Map(); // tabId → watcher
|
|
485
524
|
let _sessionListTimer = null;
|
|
525
|
+
let _lastSessionListJson = null; // diff-guard: skip push when snapshot unchanged
|
|
486
526
|
|
|
487
527
|
/** Push an unsolicited event to the browser(s). Encrypts when an E2E key is active. */
|
|
488
528
|
function pushEvent(type, payload) {
|
|
489
529
|
if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
|
|
490
530
|
const inner = { type, id: crypto.randomUUID(), payload, ts: Date.now() };
|
|
491
531
|
try {
|
|
492
|
-
if (
|
|
493
|
-
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner),
|
|
532
|
+
if (_e2e.sessionKey) {
|
|
533
|
+
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
|
|
494
534
|
_ws.send(JSON.stringify({ type: 'e2e:box', id: inner.id, payload: { nonce, ciphertext }, ts: Date.now() }));
|
|
495
535
|
} else {
|
|
496
536
|
_ws.send(JSON.stringify(inner));
|
|
@@ -536,12 +576,21 @@ async function tailLines(filePath, fromOffset) {
|
|
|
536
576
|
if (stat.size <= start) return { lines: [], size: stat.size, inode: stat.ino };
|
|
537
577
|
const fd = await fsp.open(filePath, 'r');
|
|
538
578
|
try {
|
|
579
|
+
// Drop the first fragment only when offset genuinely landed mid-line —
|
|
580
|
+
// i.e. the byte immediately before `start` is not '\n'. When `start`
|
|
581
|
+
// sits right on a newline boundary (previous poll ended at a complete
|
|
582
|
+
// line) the first split-part is already a full line and must be kept.
|
|
583
|
+
let dropFirst = false;
|
|
584
|
+
if (start > 0) {
|
|
585
|
+
const prev = Buffer.alloc(1);
|
|
586
|
+
await fd.read(prev, 0, 1, start - 1);
|
|
587
|
+
dropFirst = prev[0] !== 0x0a; // 0x0a = '\n'
|
|
588
|
+
}
|
|
539
589
|
const len = stat.size - start;
|
|
540
590
|
const buf = Buffer.alloc(len);
|
|
541
591
|
await fd.read(buf, 0, len, start);
|
|
542
592
|
const parts = buf.toString('utf8').split('\n').filter(Boolean);
|
|
543
|
-
|
|
544
|
-
if (start > 0 && parts.length) parts.shift();
|
|
593
|
+
if (dropFirst && parts.length) parts.shift();
|
|
545
594
|
return { lines: parts, size: stat.size, inode: stat.ino };
|
|
546
595
|
} finally {
|
|
547
596
|
await fd.close();
|
|
@@ -580,7 +629,10 @@ async function pollSessionWatcher(w) {
|
|
|
580
629
|
|
|
581
630
|
if (nextState && nextState !== w.state) {
|
|
582
631
|
w.state = nextState;
|
|
583
|
-
|
|
632
|
+
// Guard: don't push state if SAS not yet confirmed (watcher may outlive an auth reset).
|
|
633
|
+
if (_e2e.state === 'authenticated') {
|
|
634
|
+
pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
|
|
635
|
+
}
|
|
584
636
|
}
|
|
585
637
|
if (newAssistantText && newMsgId !== w.lastMsgId) {
|
|
586
638
|
w.lastAssistantText = newAssistantText;
|
|
@@ -638,6 +690,11 @@ async function pushSessionList() {
|
|
|
638
690
|
// this stops the unsolicited background push too.
|
|
639
691
|
const cfg = await loadConfig();
|
|
640
692
|
if (!cfg.remoteEnabled) return;
|
|
693
|
+
// Don't push before SAS is confirmed — session cwds/titles are sensitive user data.
|
|
694
|
+
// A relay that completes e2e:hello before the user confirms the SAS would otherwise
|
|
695
|
+
// receive the full session list immediately (same threat SAS_GATED_READS blocks for
|
|
696
|
+
// cmd:sessions:load). Guard here so _lastSessionListJson is not poisoned either.
|
|
697
|
+
if (_e2e.state !== 'authenticated') return;
|
|
641
698
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
642
699
|
const data = await sessionsStore.load();
|
|
643
700
|
// Normalize persisted tabs → SessionMeta. tabId === claudeSessionId so it
|
|
@@ -648,7 +705,11 @@ async function pushSessionList() {
|
|
|
648
705
|
title: t.label || t.cwd,
|
|
649
706
|
state: _sessionWatchers.get(t.claudeSessionId)?.state ?? null,
|
|
650
707
|
}));
|
|
651
|
-
|
|
708
|
+
const payload = { sessions, activeTabId: data?.activeTabId ?? null };
|
|
709
|
+
const json = JSON.stringify(payload);
|
|
710
|
+
if (json === _lastSessionListJson) return;
|
|
711
|
+
_lastSessionListJson = json;
|
|
712
|
+
pushEvent('event:session:list', payload);
|
|
652
713
|
} catch (e) {
|
|
653
714
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
|
|
654
715
|
}
|
|
@@ -734,6 +795,8 @@ function anthropicSummarize(apiKey, text) {
|
|
|
734
795
|
* completed turn per subscribed tab (~$1/$5 per 1M tokens).
|
|
735
796
|
*/
|
|
736
797
|
async function maybeSummarize(w) {
|
|
798
|
+
// Guard: don't push summaries (session transcript content) before SAS confirmed.
|
|
799
|
+
if (_e2e.state !== 'authenticated') return;
|
|
737
800
|
const text = w.lastAssistantText;
|
|
738
801
|
if (!text) return;
|
|
739
802
|
const ofMessageId = w.lastMsgId;
|
|
@@ -822,28 +885,69 @@ async function handleMessage(raw, device) {
|
|
|
822
885
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello but no device private key — skipping E2E' });
|
|
823
886
|
return;
|
|
824
887
|
}
|
|
888
|
+
// Explicit P-256 curve validation — do not rely on Node's implicit throw.
|
|
889
|
+
// Rejects wrong-curve keys (e.g. P-384), malformed DER, and the all-zero
|
|
890
|
+
// identity point. Must happen before deriveSessionKey so a bad key drops
|
|
891
|
+
// the session here, not silently in a crypto catch-all below.
|
|
825
892
|
try {
|
|
826
|
-
|
|
827
|
-
|
|
893
|
+
const derBytes = Buffer.from(browserPubKey, 'base64url');
|
|
894
|
+
const importedPub = crypto.createPublicKey({ key: derBytes, format: 'der', type: 'spki' });
|
|
895
|
+
if (importedPub.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
|
|
896
|
+
throw new Error(`wrong curve: ${importedPub.asymmetricKeyDetails?.namedCurve}`);
|
|
897
|
+
}
|
|
898
|
+
// P-256 SPKI DER is always 91 bytes; raw EC point (04 || x || y) starts at offset 26.
|
|
899
|
+
// Defense-in-depth: reject the identity point (x=0, y=0) explicitly even though
|
|
900
|
+
// Node's ECDH would also reject it — P-256 is a prime-order group, so the identity
|
|
901
|
+
// is the only low-order point.
|
|
902
|
+
if (derBytes.length === 91) {
|
|
903
|
+
const x = derBytes.subarray(27, 59);
|
|
904
|
+
const y = derBytes.subarray(59, 91);
|
|
905
|
+
if (x.every((b) => b === 0) && y.every((b) => b === 0)) {
|
|
906
|
+
throw new Error('identity point rejected');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch (e) {
|
|
910
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello peer key validation failed — session dropped', meta: { error: e?.message } });
|
|
911
|
+
await auditLog(new Date().toISOString(), 'e2e:hello', device.deviceId, undefined, 'error:invalid_peer_key');
|
|
912
|
+
resetE2e('failed'); // surface key-validation failure to UI — user must reconnect
|
|
913
|
+
broadcastStatus();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const sessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
|
|
918
|
+
let pendingSas;
|
|
919
|
+
try {
|
|
920
|
+
pendingSas = deriveSas(device.e2ePrivateKey, browserPubKey, device.deviceId);
|
|
921
|
+
} catch (sasErr) {
|
|
922
|
+
// SAS derivation failed — session cannot be verified by the user.
|
|
923
|
+
// Mark failed so confirm-sas returns ok:false and the UI prompts retry.
|
|
924
|
+
resetE2e('failed');
|
|
925
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E SAS derivation failed — session marked failed', meta: { error: sasErr?.message } });
|
|
926
|
+
broadcastStatus();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
_e2e = makeState('pending_sas', sessionKey, pendingSas);
|
|
930
|
+
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established — SAS pending confirmation' });
|
|
828
931
|
broadcastStatus();
|
|
829
932
|
// Acknowledge with e2e:ready (unencrypted — session just started)
|
|
830
933
|
respond(id, undefined, 'e2e:ready');
|
|
831
934
|
} catch (e) {
|
|
832
935
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E key derivation failed', meta: { error: e?.message } });
|
|
833
|
-
|
|
936
|
+
resetE2e('failed');
|
|
937
|
+
broadcastStatus();
|
|
834
938
|
}
|
|
835
939
|
return;
|
|
836
940
|
}
|
|
837
941
|
|
|
838
942
|
// ── Decrypt e2e:box messages ──────────────────────────────────────────────
|
|
839
943
|
if (type === 'e2e:box') {
|
|
840
|
-
if (!
|
|
944
|
+
if (!_e2e.sessionKey) {
|
|
841
945
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box received but no session key — dropping' });
|
|
842
946
|
return;
|
|
843
947
|
}
|
|
844
948
|
const { nonce, ciphertext } = payload || {};
|
|
845
949
|
if (!nonce || !ciphertext) return;
|
|
846
|
-
const plaintext = decryptBox(nonce, ciphertext,
|
|
950
|
+
const plaintext = decryptBox(nonce, ciphertext, _e2e.sessionKey);
|
|
847
951
|
if (!plaintext) {
|
|
848
952
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box decryption failed (auth tag mismatch)' });
|
|
849
953
|
return;
|
|
@@ -864,7 +968,7 @@ async function handleMessage(raw, device) {
|
|
|
864
968
|
if (!type.startsWith('cmd:')) return;
|
|
865
969
|
// After E2E is established, reject plaintext commands — a malicious relay cannot
|
|
866
970
|
// silently downgrade the session by stripping e2e:hello (PENTEST.md §H1).
|
|
867
|
-
if (
|
|
971
|
+
if (_e2e.sessionKey) {
|
|
868
972
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'plaintext cmd rejected — e2e session active' });
|
|
869
973
|
return;
|
|
870
974
|
}
|
|
@@ -881,13 +985,31 @@ async function dispatchEnvelope(envelope, device) {
|
|
|
881
985
|
const cfg = await loadConfig();
|
|
882
986
|
if (!cfg.remoteEnabled) {
|
|
883
987
|
await auditLog(ts, type, device.deviceId, id, 'error:disabled');
|
|
884
|
-
respond(id, { error: '
|
|
988
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
885
989
|
return;
|
|
886
990
|
}
|
|
887
991
|
|
|
888
|
-
// Allowlist check — unknown types
|
|
992
|
+
// Allowlist check — reject unknown cmd:* types with opaque error (oracle prevention)
|
|
889
993
|
if (!ALLOWED_COMMANDS.has(type)) {
|
|
890
994
|
await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
|
|
995
|
+
respond(id, { error: 'rejected' });
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// MUTATE tier gate — write/exec commands require remoteControlEnabled=true (default false)
|
|
1000
|
+
if (MUTATE_COMMANDS.has(type) && !cfg.remoteControlEnabled) {
|
|
1001
|
+
await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
|
|
1002
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// E2E auth gate — MUTATE and sensitive READ commands are blocked until the
|
|
1007
|
+
// user confirms the SAS on the desktop. This prevents a compromised relay
|
|
1008
|
+
// from exfiltrating session lists, PRDs, run logs, or transcript summaries
|
|
1009
|
+
// by completing the ECDH handshake without the user's knowledge.
|
|
1010
|
+
if ((MUTATE_COMMANDS.has(type) || SAS_GATED_READS.has(type)) && _e2e.state !== 'authenticated') {
|
|
1011
|
+
await auditLog(ts, type, device.deviceId, id, 'error:e2e_not_authenticated');
|
|
1012
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
891
1013
|
return;
|
|
892
1014
|
}
|
|
893
1015
|
|
|
@@ -899,7 +1021,7 @@ async function dispatchEnvelope(envelope, device) {
|
|
|
899
1021
|
} catch (e) {
|
|
900
1022
|
const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
|
|
901
1023
|
await auditLog(ts, type, device.deviceId, id, `error:${code}`);
|
|
902
|
-
result = { error:
|
|
1024
|
+
result = { error: 'rejected' }; // opaque on-wire — reason stays in audit log only
|
|
903
1025
|
}
|
|
904
1026
|
|
|
905
1027
|
respond(id, result);
|
|
@@ -919,8 +1041,8 @@ function respond(msgId, payload, typeOverride) {
|
|
|
919
1041
|
|
|
920
1042
|
try {
|
|
921
1043
|
// Encrypt the response if a session key is active.
|
|
922
|
-
if (
|
|
923
|
-
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner),
|
|
1044
|
+
if (_e2e.sessionKey && !typeOverride) {
|
|
1045
|
+
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
|
|
924
1046
|
_ws.send(JSON.stringify({
|
|
925
1047
|
type: 'e2e:box',
|
|
926
1048
|
id: msgId,
|
|
@@ -1139,16 +1261,41 @@ function registerRemoteHandlers() {
|
|
|
1139
1261
|
// Returns current status without tokens — safe to expose to renderer.
|
|
1140
1262
|
ipcMain.handle('webRemote:get-status', async () => {
|
|
1141
1263
|
const cfg = await loadConfig();
|
|
1264
|
+
const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
|
|
1142
1265
|
return {
|
|
1143
1266
|
enabled: cfg.remoteEnabled,
|
|
1144
|
-
|
|
1145
|
-
|
|
1267
|
+
remoteControlEnabled: cfg.remoteControlEnabled ?? false,
|
|
1268
|
+
connected,
|
|
1269
|
+
e2eActive: connected && _e2e.sessionKey !== null,
|
|
1270
|
+
e2eAuthenticated: connected && _e2e.state === 'authenticated',
|
|
1271
|
+
e2eState: connected ? _e2e.state : 'idle',
|
|
1272
|
+
pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
|
|
1146
1273
|
devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
|
|
1147
1274
|
deviceId, deviceName, issuedAt, lastConnectedAt,
|
|
1148
1275
|
})),
|
|
1149
1276
|
};
|
|
1150
1277
|
});
|
|
1151
1278
|
|
|
1279
|
+
// User confirmed that the SAS shown on both desktop and browser match.
|
|
1280
|
+
// Returns ok:false if the session is not in the pending_sas state (e.g. key
|
|
1281
|
+
// missing, already failed, already authenticated, or reconnected).
|
|
1282
|
+
ipcMain.handle('webRemote:confirm-sas', async () => {
|
|
1283
|
+
const { ok, error, next } = confirmSasLogic(_e2e);
|
|
1284
|
+
if (!ok) {
|
|
1285
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'confirm-sas rejected — wrong state', meta: { state: _e2e.state, error } });
|
|
1286
|
+
return { ok: false, error };
|
|
1287
|
+
}
|
|
1288
|
+
_e2e = next;
|
|
1289
|
+
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session authenticated — SAS confirmed by user' });
|
|
1290
|
+
broadcastStatus();
|
|
1291
|
+
// Flush the session list immediately — the push loop was suppressed while
|
|
1292
|
+
// state !== 'authenticated', so the mobile app would otherwise wait up to
|
|
1293
|
+
// SESSION_LIST_PUSH_MS for the first useful data.
|
|
1294
|
+
_lastSessionListJson = null;
|
|
1295
|
+
pushSessionList().catch(() => {});
|
|
1296
|
+
return { ok: true };
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1152
1299
|
ipcMain.handle('webRemote:enable', async () => {
|
|
1153
1300
|
const cfg = await loadConfig();
|
|
1154
1301
|
await saveConfig({ ...cfg, remoteEnabled: true });
|
|
@@ -1165,6 +1312,20 @@ function registerRemoteHandlers() {
|
|
|
1165
1312
|
return { ok: true };
|
|
1166
1313
|
});
|
|
1167
1314
|
|
|
1315
|
+
ipcMain.handle('webRemote:enable-control', async () => {
|
|
1316
|
+
const cfg = await loadConfig();
|
|
1317
|
+
await saveConfig({ ...cfg, remoteControlEnabled: true });
|
|
1318
|
+
broadcastStatus();
|
|
1319
|
+
return { ok: true };
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
ipcMain.handle('webRemote:disable-control', async () => {
|
|
1323
|
+
const cfg = await loadConfig();
|
|
1324
|
+
await saveConfig({ ...cfg, remoteControlEnabled: false });
|
|
1325
|
+
broadcastStatus();
|
|
1326
|
+
return { ok: true };
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1168
1329
|
ipcMain.handle('webRemote:pair', validated(schemas.webRemotePair, async ({ otp }) => {
|
|
1169
1330
|
return pair(otp);
|
|
1170
1331
|
}));
|
|
@@ -1212,7 +1373,7 @@ async function init() {
|
|
|
1212
1373
|
function destroy() {
|
|
1213
1374
|
_destroyed = true;
|
|
1214
1375
|
cancelReconnect();
|
|
1215
|
-
|
|
1376
|
+
resetE2e();
|
|
1216
1377
|
if (_ws) {
|
|
1217
1378
|
try { _ws.terminate(); } catch { /* */ }
|
|
1218
1379
|
_ws = null;
|
package/src/preload/api.d.ts
CHANGED
|
@@ -393,6 +393,13 @@ export interface SupervisorLogEntry {
|
|
|
393
393
|
costUsd: number | null;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
export interface SchedulePollHealth {
|
|
397
|
+
lastPollAt: number | null;
|
|
398
|
+
lastPollOk: boolean;
|
|
399
|
+
consecutiveFailures: number;
|
|
400
|
+
lastFailureKind: string | null;
|
|
401
|
+
}
|
|
402
|
+
|
|
396
403
|
export interface ScheduleStateSnapshot {
|
|
397
404
|
config: ScheduleConfig & { supervisor?: SupervisorConfig };
|
|
398
405
|
jobs: ScheduleJob[];
|
|
@@ -403,6 +410,8 @@ export interface ScheduleStateSnapshot {
|
|
|
403
410
|
paused: SchedulePauseInfo | null;
|
|
404
411
|
/** Latest five_hour utilization percent (0–100) cached from billing.fetchUsage. null if unknown. */
|
|
405
412
|
utilization: number | null;
|
|
413
|
+
/** Poll health — last billing poll result; used to detect stale utilization. */
|
|
414
|
+
pollHealth?: SchedulePollHealth;
|
|
406
415
|
/** Returned only by the initial state() call, not the broadcast event. */
|
|
407
416
|
paths?: SchedulePaths;
|
|
408
417
|
}
|
|
@@ -452,6 +461,19 @@ export interface ListConversationsResult {
|
|
|
452
461
|
scannedMs: number;
|
|
453
462
|
}
|
|
454
463
|
|
|
464
|
+
export interface SessionScanEntry {
|
|
465
|
+
sessionId: string;
|
|
466
|
+
projectEncoded: string;
|
|
467
|
+
path: string;
|
|
468
|
+
mtimeMs: number;
|
|
469
|
+
sizeBytes: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface SessionScanResult {
|
|
473
|
+
sessions: SessionScanEntry[];
|
|
474
|
+
scannedMs: number;
|
|
475
|
+
}
|
|
476
|
+
|
|
455
477
|
|
|
456
478
|
export interface FileEntry {
|
|
457
479
|
name: string;
|
|
@@ -793,8 +815,15 @@ export interface WebRemoteDevice {
|
|
|
793
815
|
|
|
794
816
|
export interface WebRemoteStatus {
|
|
795
817
|
enabled: boolean;
|
|
818
|
+
remoteControlEnabled: boolean;
|
|
796
819
|
connected: boolean;
|
|
797
820
|
e2eActive: boolean;
|
|
821
|
+
/** True once the user has confirmed the SAS on the desktop. */
|
|
822
|
+
e2eAuthenticated: boolean;
|
|
823
|
+
/** Explicit E2E session state for UI rendering. */
|
|
824
|
+
e2eState: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
|
|
825
|
+
/** 6-digit Short Authentication String pending user confirmation, or null. */
|
|
826
|
+
pendingSas: string | null;
|
|
798
827
|
devices: WebRemoteDevice[];
|
|
799
828
|
}
|
|
800
829
|
|
|
@@ -858,7 +887,10 @@ export interface SessionManagerAPI {
|
|
|
858
887
|
};
|
|
859
888
|
transcripts: {
|
|
860
889
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
890
|
+
/** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
|
|
861
891
|
unsubscribe: (tabId: string) => Promise<{ ok: boolean }>;
|
|
892
|
+
/** Permanently destroy the sub (genuine tab close). */
|
|
893
|
+
closeTab: (tabId: string) => Promise<{ ok: boolean }>;
|
|
862
894
|
buffer: (tabId: string) => Promise<TranscriptEvent[]>;
|
|
863
895
|
pathFor: (cwd: string, sessionUuid: string) => Promise<string>;
|
|
864
896
|
onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
|
|
@@ -949,6 +981,7 @@ export interface SessionManagerAPI {
|
|
|
949
981
|
history: {
|
|
950
982
|
aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
|
|
951
983
|
listConversations: () => Promise<ListConversationsResult>;
|
|
984
|
+
scanProjects: () => Promise<SessionScanResult>;
|
|
952
985
|
};
|
|
953
986
|
schedule: {
|
|
954
987
|
state: () => Promise<ScheduleStateSnapshot>;
|
|
@@ -1062,6 +1095,10 @@ export interface SessionManagerAPI {
|
|
|
1062
1095
|
getStatus: () => Promise<WebRemoteStatus>;
|
|
1063
1096
|
enable: () => Promise<WebRemoteMutationResult>;
|
|
1064
1097
|
disable: () => Promise<WebRemoteMutationResult>;
|
|
1098
|
+
/** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
|
|
1099
|
+
enableControl: () => Promise<WebRemoteMutationResult>;
|
|
1100
|
+
/** Block MUTATE-tier commands — mobile becomes read-only mirror. */
|
|
1101
|
+
disableControl: () => Promise<WebRemoteMutationResult>;
|
|
1065
1102
|
pair: (otp: string) => Promise<WebRemotePairResult>;
|
|
1066
1103
|
revokeDevice: (deviceId: string) => Promise<WebRemoteMutationResult>;
|
|
1067
1104
|
auditTail: (lines?: number) => Promise<WebRemoteAuditTailResult>;
|
|
@@ -1071,6 +1108,9 @@ export interface SessionManagerAPI {
|
|
|
1071
1108
|
revokeAll: () => Promise<WebRemoteMutationResult>;
|
|
1072
1109
|
/** Subscribe to the completion event broadcast after revokeAll. */
|
|
1073
1110
|
onRevokedAll: (handler: (ev: { revokedCount: number }) => void) => () => void;
|
|
1111
|
+
/** Confirm that the SAS shown on the desktop matches the browser — marks the
|
|
1112
|
+
* E2E session as authenticated and unblocks MUTATE-tier commands. */
|
|
1113
|
+
confirmSas: () => Promise<WebRemoteMutationResult>;
|
|
1074
1114
|
};
|
|
1075
1115
|
kg: {
|
|
1076
1116
|
/** Distilled knowledge graph + ingest status for ONE project (`cwd`).
|
package/src/preload/index.cjs
CHANGED
|
@@ -59,6 +59,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
59
59
|
transcripts: {
|
|
60
60
|
subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
|
|
61
61
|
unsubscribe: (tabId) => ipcRenderer.invoke('transcript:unsubscribe', { tabId }),
|
|
62
|
+
closeTab: (tabId) => ipcRenderer.invoke('transcript:close', { tabId }),
|
|
62
63
|
buffer: (tabId) => ipcRenderer.invoke('transcript:buffer', { tabId }),
|
|
63
64
|
pathFor: (cwd, sessionUuid) =>
|
|
64
65
|
ipcRenderer.invoke('transcript:path', { cwd, sessionUuid }),
|
|
@@ -155,6 +156,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
155
156
|
history: {
|
|
156
157
|
aggregate: (req) => ipcRenderer.invoke('history:aggregate', req),
|
|
157
158
|
listConversations: () => ipcRenderer.invoke('history:list-conversations'),
|
|
159
|
+
scanProjects: () => ipcRenderer.invoke('history:scan-projects'),
|
|
158
160
|
},
|
|
159
161
|
files: {
|
|
160
162
|
list: (path, showHidden) => ipcRenderer.invoke('files:list', { path, showHidden }),
|
|
@@ -283,6 +285,10 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
283
285
|
enable: () => ipcRenderer.invoke('webRemote:enable'),
|
|
284
286
|
/** Turn remote control off. Immediately drops relay connection. */
|
|
285
287
|
disable: () => ipcRenderer.invoke('webRemote:disable'),
|
|
288
|
+
/** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
|
|
289
|
+
enableControl: () => ipcRenderer.invoke('webRemote:enable-control'),
|
|
290
|
+
/** Block MUTATE-tier commands — mobile becomes read-only mirror. */
|
|
291
|
+
disableControl: () => ipcRenderer.invoke('webRemote:disable-control'),
|
|
286
292
|
/** Pair a new device using the 8-character OTP shown in the web UI. */
|
|
287
293
|
pair: (otp) => ipcRenderer.invoke('webRemote:pair', { otp }),
|
|
288
294
|
/** Revoke a paired device by its deviceId. */
|
|
@@ -303,6 +309,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
303
309
|
},
|
|
304
310
|
/** Revoke ALL paired devices and tear down every session immediately. */
|
|
305
311
|
revokeAll: () => ipcRenderer.invoke('webRemote:revoke-all'),
|
|
312
|
+
confirmSas: () => ipcRenderer.invoke('webRemote:confirm-sas'),
|
|
306
313
|
/** Push event when revokeAll completes (main broadcasts webRemote:revoked-all). */
|
|
307
314
|
onRevokedAll: (handler) => {
|
|
308
315
|
const listener = (_e, payload) => handler(payload);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as h,l as s}from"./index-DMobTczM.js";import{C as c,H as u,d as p,D as m,R as f,g as _,h as w,b as k,F as v,a as D,S as P,c as R,f as I}from"./lspLanguageFeatures-CRF3U0x3.js";import{e as b,i as H,j as U,t as y,k as T}from"./lspLanguageFeatures-CRF3U0x3.js";const C=120*1e3;class A{constructor(o){this._defaults=o,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>C&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=h({moduleId:"vs/language/css/cssWorker",createWorker:()=>new Worker(new URL(""+new URL("css.worker-B4z49cGk.js",import.meta.url).href,import.meta.url),{type:"module"}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...o){let e;return this._getClient().then(a=>{e=a}).then(a=>{if(this._worker)return this._worker.withSyncedResources(o)}).then(a=>e)}}function F(n){const o=[],e=[],a=new A(n);o.push(a);const r=(...t)=>a.getLanguageServiceWorker(...t);function l(){const{languageId:t,modeConfiguration:i}=n;g(e),i.completionItems&&e.push(s.registerCompletionItemProvider(t,new c(r,["/","-",":"]))),i.hovers&&e.push(s.registerHoverProvider(t,new u(r))),i.documentHighlights&&e.push(s.registerDocumentHighlightProvider(t,new p(r))),i.definitions&&e.push(s.registerDefinitionProvider(t,new m(r))),i.references&&e.push(s.registerReferenceProvider(t,new f(r))),i.documentSymbols&&e.push(s.registerDocumentSymbolProvider(t,new _(r))),i.rename&&e.push(s.registerRenameProvider(t,new w(r))),i.colors&&e.push(s.registerColorProvider(t,new k(r))),i.foldingRanges&&e.push(s.registerFoldingRangeProvider(t,new v(r))),i.diagnostics&&e.push(new D(t,r,n.onDidChange)),i.selectionRanges&&e.push(s.registerSelectionRangeProvider(t,new P(r))),i.documentFormattingEdits&&e.push(s.registerDocumentFormattingEditProvider(t,new R(r))),i.documentRangeFormattingEdits&&e.push(s.registerDocumentRangeFormattingEditProvider(t,new I(r)))}return l(),o.push(d(e)),d(o)}function d(n){return{dispose:()=>g(n)}}function g(n){for(;n.length;)n.pop().dispose()}export{c as CompletionAdapter,m as DefinitionAdapter,D as DiagnosticsAdapter,k as DocumentColorAdapter,R as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,b as DocumentLinkAdapter,I as DocumentRangeFormattingEditProvider,_ as DocumentSymbolAdapter,v as FoldingRangeAdapter,u as HoverAdapter,f as ReferenceAdapter,w as RenameAdapter,P as SelectionRangeAdapter,A as WorkerManager,H as fromPosition,U as fromRange,F as setupMode,y as toRange,T as toTextEdit};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as e}from"./index-DMobTczM.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],a={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[["<!--","-->"],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/<!DOCTYPE/,"metatag.html","@doctype"],[/<!--/,"comment.html","@commentHtml"],[/(<)(\w+)(\/>)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/</,"delimiter.html"],[/\{/,"delimiter.html"],[/[^<{]+/]],doctype:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/[^>]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}};export{a as conf,m as language};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as e}from"./index-DMobTczM.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["<!--","-->"]},brackets:[["<!--","-->"],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*<!--\\s*#region\\b.*-->"),end:new RegExp("^\\s*<!--\\s*#endregion\\b.*-->")}}},r={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/<!DOCTYPE/,"metatag","@doctype"],[/<!--/,"comment","@comment"],[/(<)((?:[\w\-]+:)?[\w\-]+)(\s*)(\/>)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/</,"delimiter"],[/[^<]+/]],doctype:[[/[^>]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"module"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/'module'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}};export{i as conf,r as language};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as D,l as t}from"./index-DMobTczM.js";import{H as d,d as l,e as u,F as c,g as h,S as m,h as p,c as w,f as _,C as R}from"./lspLanguageFeatures-CRF3U0x3.js";import{D as E,a as H,b,R as y,i as U,j as T,t as x,k as M}from"./lspLanguageFeatures-CRF3U0x3.js";const I=120*1e3;class f{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>I&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=D({moduleId:"vs/language/html/htmlWorker",createWorker:()=>new Worker(new URL(""+new URL("html.worker-DtiGdgqp.js",import.meta.url).href,import.meta.url),{type:"module"}),createData:{languageSettings:this._defaults.options,languageId:this._defaults.languageId},label:this._defaults.languageId}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(r=>{e=r}).then(r=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(r=>e)}}class v extends R{constructor(n){super(n,[".",":","<",'"',"=","/"])}}function C(i){const n=new f(i),e=(...o)=>n.getLanguageServiceWorker(...o);let r=i.languageId;t.registerCompletionItemProvider(r,new v(e)),t.registerHoverProvider(r,new d(e)),t.registerDocumentHighlightProvider(r,new l(e)),t.registerLinkProvider(r,new u(e)),t.registerFoldingRangeProvider(r,new c(e)),t.registerDocumentSymbolProvider(r,new h(e)),t.registerSelectionRangeProvider(r,new m(e)),t.registerRenameProvider(r,new p(e)),r==="html"&&(t.registerDocumentFormattingEditProvider(r,new w(e)),t.registerDocumentRangeFormattingEditProvider(r,new _(e)))}function L(i){const n=[],e=[],r=new f(i);n.push(r);const o=(...s)=>r.getLanguageServiceWorker(...s);function P(){const{languageId:s,modeConfiguration:a}=i;k(e),a.completionItems&&e.push(t.registerCompletionItemProvider(s,new v(o))),a.hovers&&e.push(t.registerHoverProvider(s,new d(o))),a.documentHighlights&&e.push(t.registerDocumentHighlightProvider(s,new l(o))),a.links&&e.push(t.registerLinkProvider(s,new u(o))),a.documentSymbols&&e.push(t.registerDocumentSymbolProvider(s,new h(o))),a.rename&&e.push(t.registerRenameProvider(s,new p(o))),a.foldingRanges&&e.push(t.registerFoldingRangeProvider(s,new c(o))),a.selectionRanges&&e.push(t.registerSelectionRangeProvider(s,new m(o))),a.documentFormattingEdits&&e.push(t.registerDocumentFormattingEditProvider(s,new w(o))),a.documentRangeFormattingEdits&&e.push(t.registerDocumentRangeFormattingEditProvider(s,new _(o)))}return P(),n.push(g(e)),g(n)}function g(i){return{dispose:()=>k(i)}}function k(i){for(;i.length;)i.pop().dispose()}export{R as CompletionAdapter,E as DefinitionAdapter,H as DiagnosticsAdapter,b as DocumentColorAdapter,w as DocumentFormattingEditProvider,l as DocumentHighlightAdapter,u as DocumentLinkAdapter,_ as DocumentRangeFormattingEditProvider,h as DocumentSymbolAdapter,c as FoldingRangeAdapter,d as HoverAdapter,y as ReferenceAdapter,p as RenameAdapter,m as SelectionRangeAdapter,f as WorkerManager,U as fromPosition,T as fromRange,L as setupMode,C as setupMode1,x as toRange,M as toTextEdit};
|