claude-code-session-manager 0.21.2 → 0.21.4
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-CepFtp62.js → TiptapBody-CZLSQ6pj.js} +2 -2
- package/dist/assets/cssMode-DfqZGMQs.js +1 -0
- package/dist/assets/{freemarker2-DqQlU_4i.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-BUrrcj7x.js +3525 -0
- package/dist/assets/index-DeQI4oVI.css +32 -0
- package/dist/assets/javascript-BVxRZMds.js +1 -0
- package/dist/assets/{jsonMode-CFEryxme.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-CrE67_1W.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-CNLm8WAZ.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 +138 -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 +51 -5
- package/src/main/scheduler/prdParser.cjs +16 -1
- package/src/main/scheduler.cjs +171 -13
- package/src/main/transcripts.cjs +141 -19
- package/src/main/usageMatrix.cjs +7 -3
- package/src/main/webRemote.cjs +196 -31
- package/src/preload/api.d.ts +40 -0
- package/src/preload/index.cjs +7 -0
- package/dist/assets/cssMode-8hR_Zezu.js +0 -1
- package/dist/assets/handlebars-Ts2NzFcS.js +0 -1
- package/dist/assets/html-QjLxt2p6.js +0 -1
- package/dist/assets/htmlMode-Dst38sy3.js +0 -1
- package/dist/assets/index-XKsJ4Pk3.js +0 -4431
- package/dist/assets/javascript-CNxLjNGz.js +0 -1
- package/dist/assets/liquid-BBfKLTB_.js +0 -1
- package/dist/assets/lspLanguageFeatures-BNyh7ouG.js +0 -4
- package/dist/assets/mdx-SaTyS1xC.js +0 -1
- package/dist/assets/python-C84TNhMd.js +0 -1
- package/dist/assets/razor-BaVJM3L8.js +0 -1
- package/dist/assets/typescript-BdrDpzPy.js +0 -1
- package/dist/assets/xml-CHJ3Xjjj.js +0 -1
- package/dist/assets/yaml-Cg2-K8t3.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,13 +576,26 @@ 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
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
592
|
+
// Shift before filter: the fragment may be an empty string (when the
|
|
593
|
+
// buffer starts with '\n', completing the previous partial line). If we
|
|
594
|
+
// filter(Boolean) first, the empty fragment disappears and shift() would
|
|
595
|
+
// remove the first valid line instead.
|
|
596
|
+
const parts = buf.toString('utf8').split('\n');
|
|
597
|
+
if (dropFirst && parts.length) parts.shift();
|
|
598
|
+
return { lines: parts.filter(Boolean), size: stat.size, inode: stat.ino };
|
|
546
599
|
} finally {
|
|
547
600
|
await fd.close();
|
|
548
601
|
}
|
|
@@ -580,7 +633,10 @@ async function pollSessionWatcher(w) {
|
|
|
580
633
|
|
|
581
634
|
if (nextState && nextState !== w.state) {
|
|
582
635
|
w.state = nextState;
|
|
583
|
-
|
|
636
|
+
// Guard: don't push state if SAS not yet confirmed (watcher may outlive an auth reset).
|
|
637
|
+
if (_e2e.state === 'authenticated') {
|
|
638
|
+
pushEvent('event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
|
|
639
|
+
}
|
|
584
640
|
}
|
|
585
641
|
if (newAssistantText && newMsgId !== w.lastMsgId) {
|
|
586
642
|
w.lastAssistantText = newAssistantText;
|
|
@@ -638,6 +694,11 @@ async function pushSessionList() {
|
|
|
638
694
|
// this stops the unsolicited background push too.
|
|
639
695
|
const cfg = await loadConfig();
|
|
640
696
|
if (!cfg.remoteEnabled) return;
|
|
697
|
+
// Don't push before SAS is confirmed — session cwds/titles are sensitive user data.
|
|
698
|
+
// A relay that completes e2e:hello before the user confirms the SAS would otherwise
|
|
699
|
+
// receive the full session list immediately (same threat SAS_GATED_READS blocks for
|
|
700
|
+
// cmd:sessions:load). Guard here so _lastSessionListJson is not poisoned either.
|
|
701
|
+
if (_e2e.state !== 'authenticated') return;
|
|
641
702
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
642
703
|
const data = await sessionsStore.load();
|
|
643
704
|
// Normalize persisted tabs → SessionMeta. tabId === claudeSessionId so it
|
|
@@ -648,7 +709,11 @@ async function pushSessionList() {
|
|
|
648
709
|
title: t.label || t.cwd,
|
|
649
710
|
state: _sessionWatchers.get(t.claudeSessionId)?.state ?? null,
|
|
650
711
|
}));
|
|
651
|
-
|
|
712
|
+
const payload = { sessions, activeTabId: data?.activeTabId ?? null };
|
|
713
|
+
const json = JSON.stringify(payload);
|
|
714
|
+
if (json === _lastSessionListJson) return;
|
|
715
|
+
_lastSessionListJson = json;
|
|
716
|
+
pushEvent('event:session:list', payload);
|
|
652
717
|
} catch (e) {
|
|
653
718
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'pushSessionList failed', meta: { error: e?.message } });
|
|
654
719
|
}
|
|
@@ -734,6 +799,8 @@ function anthropicSummarize(apiKey, text) {
|
|
|
734
799
|
* completed turn per subscribed tab (~$1/$5 per 1M tokens).
|
|
735
800
|
*/
|
|
736
801
|
async function maybeSummarize(w) {
|
|
802
|
+
// Guard: don't push summaries (session transcript content) before SAS confirmed.
|
|
803
|
+
if (_e2e.state !== 'authenticated') return;
|
|
737
804
|
const text = w.lastAssistantText;
|
|
738
805
|
if (!text) return;
|
|
739
806
|
const ofMessageId = w.lastMsgId;
|
|
@@ -822,28 +889,69 @@ async function handleMessage(raw, device) {
|
|
|
822
889
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello but no device private key — skipping E2E' });
|
|
823
890
|
return;
|
|
824
891
|
}
|
|
892
|
+
// Explicit P-256 curve validation — do not rely on Node's implicit throw.
|
|
893
|
+
// Rejects wrong-curve keys (e.g. P-384), malformed DER, and the all-zero
|
|
894
|
+
// identity point. Must happen before deriveSessionKey so a bad key drops
|
|
895
|
+
// the session here, not silently in a crypto catch-all below.
|
|
825
896
|
try {
|
|
826
|
-
|
|
827
|
-
|
|
897
|
+
const derBytes = Buffer.from(browserPubKey, 'base64url');
|
|
898
|
+
const importedPub = crypto.createPublicKey({ key: derBytes, format: 'der', type: 'spki' });
|
|
899
|
+
if (importedPub.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
|
|
900
|
+
throw new Error(`wrong curve: ${importedPub.asymmetricKeyDetails?.namedCurve}`);
|
|
901
|
+
}
|
|
902
|
+
// P-256 SPKI DER is always 91 bytes; raw EC point (04 || x || y) starts at offset 26.
|
|
903
|
+
// Defense-in-depth: reject the identity point (x=0, y=0) explicitly even though
|
|
904
|
+
// Node's ECDH would also reject it — P-256 is a prime-order group, so the identity
|
|
905
|
+
// is the only low-order point.
|
|
906
|
+
if (derBytes.length === 91) {
|
|
907
|
+
const x = derBytes.subarray(27, 59);
|
|
908
|
+
const y = derBytes.subarray(59, 91);
|
|
909
|
+
if (x.every((b) => b === 0) && y.every((b) => b === 0)) {
|
|
910
|
+
throw new Error('identity point rejected');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} catch (e) {
|
|
914
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:hello peer key validation failed — session dropped', meta: { error: e?.message } });
|
|
915
|
+
await auditLog(new Date().toISOString(), 'e2e:hello', device.deviceId, undefined, 'error:invalid_peer_key');
|
|
916
|
+
resetE2e('failed'); // surface key-validation failure to UI — user must reconnect
|
|
917
|
+
broadcastStatus();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
const sessionKey = deriveSessionKey(device.e2ePrivateKey, browserPubKey, device.deviceId);
|
|
922
|
+
let pendingSas;
|
|
923
|
+
try {
|
|
924
|
+
pendingSas = deriveSas(device.e2ePrivateKey, browserPubKey, device.deviceId);
|
|
925
|
+
} catch (sasErr) {
|
|
926
|
+
// SAS derivation failed — session cannot be verified by the user.
|
|
927
|
+
// Mark failed so confirm-sas returns ok:false and the UI prompts retry.
|
|
928
|
+
resetE2e('failed');
|
|
929
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E SAS derivation failed — session marked failed', meta: { error: sasErr?.message } });
|
|
930
|
+
broadcastStatus();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
_e2e = makeState('pending_sas', sessionKey, pendingSas);
|
|
934
|
+
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session key established — SAS pending confirmation' });
|
|
828
935
|
broadcastStatus();
|
|
829
936
|
// Acknowledge with e2e:ready (unencrypted — session just started)
|
|
830
937
|
respond(id, undefined, 'e2e:ready');
|
|
831
938
|
} catch (e) {
|
|
832
939
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'E2E key derivation failed', meta: { error: e?.message } });
|
|
833
|
-
|
|
940
|
+
resetE2e('failed');
|
|
941
|
+
broadcastStatus();
|
|
834
942
|
}
|
|
835
943
|
return;
|
|
836
944
|
}
|
|
837
945
|
|
|
838
946
|
// ── Decrypt e2e:box messages ──────────────────────────────────────────────
|
|
839
947
|
if (type === 'e2e:box') {
|
|
840
|
-
if (!
|
|
948
|
+
if (!_e2e.sessionKey) {
|
|
841
949
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box received but no session key — dropping' });
|
|
842
950
|
return;
|
|
843
951
|
}
|
|
844
952
|
const { nonce, ciphertext } = payload || {};
|
|
845
953
|
if (!nonce || !ciphertext) return;
|
|
846
|
-
const plaintext = decryptBox(nonce, ciphertext,
|
|
954
|
+
const plaintext = decryptBox(nonce, ciphertext, _e2e.sessionKey);
|
|
847
955
|
if (!plaintext) {
|
|
848
956
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'e2e:box decryption failed (auth tag mismatch)' });
|
|
849
957
|
return;
|
|
@@ -864,7 +972,7 @@ async function handleMessage(raw, device) {
|
|
|
864
972
|
if (!type.startsWith('cmd:')) return;
|
|
865
973
|
// After E2E is established, reject plaintext commands — a malicious relay cannot
|
|
866
974
|
// silently downgrade the session by stripping e2e:hello (PENTEST.md §H1).
|
|
867
|
-
if (
|
|
975
|
+
if (_e2e.sessionKey) {
|
|
868
976
|
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'plaintext cmd rejected — e2e session active' });
|
|
869
977
|
return;
|
|
870
978
|
}
|
|
@@ -881,13 +989,31 @@ async function dispatchEnvelope(envelope, device) {
|
|
|
881
989
|
const cfg = await loadConfig();
|
|
882
990
|
if (!cfg.remoteEnabled) {
|
|
883
991
|
await auditLog(ts, type, device.deviceId, id, 'error:disabled');
|
|
884
|
-
respond(id, { error: '
|
|
992
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
885
993
|
return;
|
|
886
994
|
}
|
|
887
995
|
|
|
888
|
-
// Allowlist check — unknown types
|
|
996
|
+
// Allowlist check — reject unknown cmd:* types with opaque error (oracle prevention)
|
|
889
997
|
if (!ALLOWED_COMMANDS.has(type)) {
|
|
890
998
|
await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
|
|
999
|
+
respond(id, { error: 'rejected' });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// MUTATE tier gate — write/exec commands require remoteControlEnabled=true (default false)
|
|
1004
|
+
if (MUTATE_COMMANDS.has(type) && !cfg.remoteControlEnabled) {
|
|
1005
|
+
await auditLog(ts, type, device.deviceId, id, 'error:not_allowed');
|
|
1006
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// E2E auth gate — MUTATE and sensitive READ commands are blocked until the
|
|
1011
|
+
// user confirms the SAS on the desktop. This prevents a compromised relay
|
|
1012
|
+
// from exfiltrating session lists, PRDs, run logs, or transcript summaries
|
|
1013
|
+
// by completing the ECDH handshake without the user's knowledge.
|
|
1014
|
+
if ((MUTATE_COMMANDS.has(type) || SAS_GATED_READS.has(type)) && _e2e.state !== 'authenticated') {
|
|
1015
|
+
await auditLog(ts, type, device.deviceId, id, 'error:e2e_not_authenticated');
|
|
1016
|
+
respond(id, { error: 'rejected' }); // opaque — reason stays in audit log only
|
|
891
1017
|
return;
|
|
892
1018
|
}
|
|
893
1019
|
|
|
@@ -899,7 +1025,7 @@ async function dispatchEnvelope(envelope, device) {
|
|
|
899
1025
|
} catch (e) {
|
|
900
1026
|
const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
|
|
901
1027
|
await auditLog(ts, type, device.deviceId, id, `error:${code}`);
|
|
902
|
-
result = { error:
|
|
1028
|
+
result = { error: 'rejected' }; // opaque on-wire — reason stays in audit log only
|
|
903
1029
|
}
|
|
904
1030
|
|
|
905
1031
|
respond(id, result);
|
|
@@ -919,8 +1045,8 @@ function respond(msgId, payload, typeOverride) {
|
|
|
919
1045
|
|
|
920
1046
|
try {
|
|
921
1047
|
// Encrypt the response if a session key is active.
|
|
922
|
-
if (
|
|
923
|
-
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner),
|
|
1048
|
+
if (_e2e.sessionKey && !typeOverride) {
|
|
1049
|
+
const { nonce, ciphertext } = encryptBox(JSON.stringify(inner), _e2e.sessionKey);
|
|
924
1050
|
_ws.send(JSON.stringify({
|
|
925
1051
|
type: 'e2e:box',
|
|
926
1052
|
id: msgId,
|
|
@@ -1139,16 +1265,41 @@ function registerRemoteHandlers() {
|
|
|
1139
1265
|
// Returns current status without tokens — safe to expose to renderer.
|
|
1140
1266
|
ipcMain.handle('webRemote:get-status', async () => {
|
|
1141
1267
|
const cfg = await loadConfig();
|
|
1268
|
+
const connected = _ws !== null && _ws.readyState === WebSocket.OPEN;
|
|
1142
1269
|
return {
|
|
1143
1270
|
enabled: cfg.remoteEnabled,
|
|
1144
|
-
|
|
1145
|
-
|
|
1271
|
+
remoteControlEnabled: cfg.remoteControlEnabled ?? false,
|
|
1272
|
+
connected,
|
|
1273
|
+
e2eActive: connected && _e2e.sessionKey !== null,
|
|
1274
|
+
e2eAuthenticated: connected && _e2e.state === 'authenticated',
|
|
1275
|
+
e2eState: connected ? _e2e.state : 'idle',
|
|
1276
|
+
pendingSas: connected && _e2e.state === 'pending_sas' ? _e2e.pendingSas : null,
|
|
1146
1277
|
devices: (cfg.devices || []).map(({ deviceId, deviceName, issuedAt, lastConnectedAt }) => ({
|
|
1147
1278
|
deviceId, deviceName, issuedAt, lastConnectedAt,
|
|
1148
1279
|
})),
|
|
1149
1280
|
};
|
|
1150
1281
|
});
|
|
1151
1282
|
|
|
1283
|
+
// User confirmed that the SAS shown on both desktop and browser match.
|
|
1284
|
+
// Returns ok:false if the session is not in the pending_sas state (e.g. key
|
|
1285
|
+
// missing, already failed, already authenticated, or reconnected).
|
|
1286
|
+
ipcMain.handle('webRemote:confirm-sas', async () => {
|
|
1287
|
+
const { ok, error, next } = confirmSasLogic(_e2e);
|
|
1288
|
+
if (!ok) {
|
|
1289
|
+
logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'confirm-sas rejected — wrong state', meta: { state: _e2e.state, error } });
|
|
1290
|
+
return { ok: false, error };
|
|
1291
|
+
}
|
|
1292
|
+
_e2e = next;
|
|
1293
|
+
logs.writeLine({ scope: 'webRemote', level: 'info', message: 'E2E session authenticated — SAS confirmed by user' });
|
|
1294
|
+
broadcastStatus();
|
|
1295
|
+
// Flush the session list immediately — the push loop was suppressed while
|
|
1296
|
+
// state !== 'authenticated', so the mobile app would otherwise wait up to
|
|
1297
|
+
// SESSION_LIST_PUSH_MS for the first useful data.
|
|
1298
|
+
_lastSessionListJson = null;
|
|
1299
|
+
pushSessionList().catch(() => {});
|
|
1300
|
+
return { ok: true };
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1152
1303
|
ipcMain.handle('webRemote:enable', async () => {
|
|
1153
1304
|
const cfg = await loadConfig();
|
|
1154
1305
|
await saveConfig({ ...cfg, remoteEnabled: true });
|
|
@@ -1165,6 +1316,20 @@ function registerRemoteHandlers() {
|
|
|
1165
1316
|
return { ok: true };
|
|
1166
1317
|
});
|
|
1167
1318
|
|
|
1319
|
+
ipcMain.handle('webRemote:enable-control', async () => {
|
|
1320
|
+
const cfg = await loadConfig();
|
|
1321
|
+
await saveConfig({ ...cfg, remoteControlEnabled: true });
|
|
1322
|
+
broadcastStatus();
|
|
1323
|
+
return { ok: true };
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
ipcMain.handle('webRemote:disable-control', async () => {
|
|
1327
|
+
const cfg = await loadConfig();
|
|
1328
|
+
await saveConfig({ ...cfg, remoteControlEnabled: false });
|
|
1329
|
+
broadcastStatus();
|
|
1330
|
+
return { ok: true };
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1168
1333
|
ipcMain.handle('webRemote:pair', validated(schemas.webRemotePair, async ({ otp }) => {
|
|
1169
1334
|
return pair(otp);
|
|
1170
1335
|
}));
|
|
@@ -1212,7 +1377,7 @@ async function init() {
|
|
|
1212
1377
|
function destroy() {
|
|
1213
1378
|
_destroyed = true;
|
|
1214
1379
|
cancelReconnect();
|
|
1215
|
-
|
|
1380
|
+
resetE2e();
|
|
1216
1381
|
if (_ws) {
|
|
1217
1382
|
try { _ws.terminate(); } catch { /* */ }
|
|
1218
1383
|
_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-XKsJ4Pk3.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-BNyh7ouG.js";import{e as b,i as H,j as U,t as y,k as T}from"./lspLanguageFeatures-BNyh7ouG.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-XKsJ4Pk3.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-XKsJ4Pk3.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-XKsJ4Pk3.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-BNyh7ouG.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-BNyh7ouG.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};
|