@zeph-to/cli 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-rules-fetch.d.ts.map +1 -1
- package/dist/agent-rules-fetch.js +7 -2
- package/dist/crypto.d.ts +0 -4
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +27 -8
- package/dist/input-sequencer.d.ts +1 -2
- package/dist/input-sequencer.d.ts.map +1 -1
- package/dist/input-sequencer.js +2 -3
- package/dist/listener.d.ts +35 -3
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +171 -46
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-rules-fetch.d.ts","sourceRoot":"","sources":["../src/agent-rules-fetch.ts"],"names":[],"mappings":"AAuBA,OAAO,EAEH,KAAK,iBAAiB,EACzB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,gBAAgB,QAAuC,CAAC;AACrE,eAAO,MAAM,yBAAyB,QAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-rules-fetch.d.ts","sourceRoot":"","sources":["../src/agent-rules-fetch.ts"],"names":[],"mappings":"AAuBA,OAAO,EAEH,KAAK,iBAAiB,EACzB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,gBAAgB,QAAuC,CAAC;AACrE,eAAO,MAAM,yBAAyB,QAAqB,CAAC;AAkD5D;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,iBAAiB,GAAG,IAWrE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAU9D,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAK5D,eAAO,MAAM,iBAAiB,QAAO,iBAAmC,CAAC;AACzE,eAAO,MAAM,uBAAuB,QAAO,cAA8B,CAAC;AAS1E,iBAAiB;AACjB,eAAO,MAAM,mBAAmB,QAAO,IAEtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAO,cAWxC,CAAC;AAWF,eAAO,MAAM,QAAQ,QAAO,MAK3B,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAU,YAAW,OAAO,KAAa,KAAG,OAAO,CAAC,aAAa,CA4C5F,CAAC"}
|
|
@@ -55,11 +55,16 @@ const isRule = (v) => {
|
|
|
55
55
|
return false;
|
|
56
56
|
if (typeof r.state !== 'string' || !VALID_STATES.has(r.state))
|
|
57
57
|
return false;
|
|
58
|
-
|
|
58
|
+
// Safe integers, not merely finite. These numbers arrive from the server
|
|
59
|
+
// and decide behavior: a fractional priority orders rules by values no
|
|
60
|
+
// author can reason about, 1e21 outranks every rule ever written including
|
|
61
|
+
// the kill-switch, and a fractional tailLines slices a capture at a
|
|
62
|
+
// fraction of a line.
|
|
63
|
+
if (!Number.isSafeInteger(r.priority))
|
|
59
64
|
return false;
|
|
60
65
|
if (r.region !== undefined && r.region !== 'tail' && r.region !== 'whole')
|
|
61
66
|
return false;
|
|
62
|
-
if (r.tailLines !== undefined && (
|
|
67
|
+
if (r.tailLines !== undefined && (!Number.isSafeInteger(r.tailLines) || r.tailLines < 1 || r.tailLines > 200))
|
|
63
68
|
return false;
|
|
64
69
|
if (r.contains !== undefined && !isStringArray(r.contains))
|
|
65
70
|
return false;
|
package/dist/crypto.d.ts
CHANGED
|
@@ -122,10 +122,6 @@ export interface EncryptedEphemeralPayload extends EncryptedPayload {
|
|
|
122
122
|
*/
|
|
123
123
|
export declare const initDeviceCrypto: () => Promise<string>;
|
|
124
124
|
export declare const getDevicePublicKey: () => string | null;
|
|
125
|
-
/**
|
|
126
|
-
* Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
|
|
127
|
-
* device. Requires initDeviceCrypto() to have completed.
|
|
128
|
-
*/
|
|
129
125
|
export declare const encryptEphemeral: (plaintext: string, recipientPublicKeyRaw: string) => Promise<EncryptedEphemeralPayload>;
|
|
130
126
|
/**
|
|
131
127
|
* Open an ephemeral envelope addressed to this device — the exact inverse of
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA0ED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAyB5E,CAAC;AAsCF,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mFAAmF;AACnF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAClD,eAAe,EAG8C,CAAC;AA2CjE,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAkD,CAAC;AACjG,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA4D,CAAC;AAEtG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,QAAO,IAEhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAkBA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,GAAG,MAAM,EACxB,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAE,CAsBxE,CAAC;AAcF;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,OAAO,CAAC,MAAM,CAoBjD,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAA+B,CAAC;AAyB/E,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,uBAAuB,MAAM,KAC5B,OAAO,CAAC,yBAAyB,CAKnC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,yBAAyB,KACjC,OAAO,CAAC,MAAM,CAIhB,CAAC"}
|
package/dist/crypto.js
CHANGED
|
@@ -76,11 +76,13 @@ const importKeyPair = async (exported) => {
|
|
|
76
76
|
return { publicKey, privateKey };
|
|
77
77
|
};
|
|
78
78
|
const deriveAesKey = async (privateKey, publicKey) => crypto.subtle.deriveKey({ name: 'ECDH', public: publicKey }, privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
79
|
-
const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey
|
|
79
|
+
const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey,
|
|
80
|
+
/** Pre-derived ECDH secret for this pair, when the caller keeps one. */
|
|
81
|
+
shared) => {
|
|
80
82
|
const messageKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
81
83
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
82
84
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, messageKey, new TextEncoder().encode(plaintext));
|
|
83
|
-
const sharedKey = await deriveAesKey(senderPrivateKey, recipientPublicKey);
|
|
85
|
+
const sharedKey = shared ?? await deriveAesKey(senderPrivateKey, recipientPublicKey);
|
|
84
86
|
const rawMessageKey = await crypto.subtle.exportKey('raw', messageKey);
|
|
85
87
|
const keyIv = crypto.getRandomValues(new Uint8Array(12));
|
|
86
88
|
const encryptedKey = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: keyIv }, sharedKey, rawMessageKey);
|
|
@@ -91,8 +93,10 @@ const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey) => {
|
|
|
91
93
|
keyIv: toBase64(keyIv.buffer),
|
|
92
94
|
};
|
|
93
95
|
};
|
|
94
|
-
const decrypt = async (payload, recipientPrivateKey, senderPublicKey
|
|
95
|
-
|
|
96
|
+
const decrypt = async (payload, recipientPrivateKey, senderPublicKey,
|
|
97
|
+
/** Pre-derived ECDH secret for this pair, when the caller keeps one. */
|
|
98
|
+
shared) => {
|
|
99
|
+
const sharedKey = shared ?? await deriveAesKey(recipientPrivateKey, senderPublicKey);
|
|
96
100
|
const rawMessageKey = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: new Uint8Array(fromBase64(payload.keyIv)) }, sharedKey, fromBase64(payload.encryptedKey));
|
|
97
101
|
const messageKey = await crypto.subtle.importKey('raw', rawMessageKey, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
98
102
|
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: new Uint8Array(fromBase64(payload.iv)) }, messageKey, fromBase64(payload.ciphertext));
|
|
@@ -341,11 +345,26 @@ exports.getDevicePublicKey = getDevicePublicKey;
|
|
|
341
345
|
* Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
|
|
342
346
|
* device. Requires initDeviceCrypto() to have completed.
|
|
343
347
|
*/
|
|
348
|
+
/**
|
|
349
|
+
* One imported peer key plus the ECDH secret derived with it. A stream talks to
|
|
350
|
+
* exactly one subscriber, and the input half binds to that same key, so both
|
|
351
|
+
* directions reuse one entry for the stream's life — deriving per frame AND per
|
|
352
|
+
* keystroke was two WebCrypto operations repeating the same answer.
|
|
353
|
+
*/
|
|
354
|
+
let peerCache = null;
|
|
355
|
+
const peerFor = async (raw, privateKey) => {
|
|
356
|
+
if (peerCache?.b64 === raw)
|
|
357
|
+
return peerCache;
|
|
358
|
+
const key = await importPublicKey(raw);
|
|
359
|
+
const shared = await deriveAesKey(privateKey, key);
|
|
360
|
+
peerCache = { b64: raw, key, shared };
|
|
361
|
+
return peerCache;
|
|
362
|
+
};
|
|
344
363
|
const encryptEphemeral = async (plaintext, recipientPublicKeyRaw) => {
|
|
345
364
|
if (!deviceKeyPair || !deviceExportedPublicKey)
|
|
346
365
|
throw new Error('Device crypto not initialized');
|
|
347
|
-
const
|
|
348
|
-
const payload = await encrypt(plaintext, deviceKeyPair.privateKey,
|
|
366
|
+
const peer = await peerFor(recipientPublicKeyRaw, deviceKeyPair.privateKey);
|
|
367
|
+
const payload = await encrypt(plaintext, deviceKeyPair.privateKey, peer.key, peer.shared);
|
|
349
368
|
return { ...payload, senderPublicKey: deviceExportedPublicKey };
|
|
350
369
|
};
|
|
351
370
|
exports.encryptEphemeral = encryptEphemeral;
|
|
@@ -367,7 +386,7 @@ exports.encryptEphemeral = encryptEphemeral;
|
|
|
367
386
|
const decryptEphemeral = async (payload) => {
|
|
368
387
|
if (!deviceKeyPair)
|
|
369
388
|
throw new Error('Device crypto not initialized');
|
|
370
|
-
const
|
|
371
|
-
return decrypt(payload, deviceKeyPair.privateKey,
|
|
389
|
+
const peer = await peerFor(payload.senderPublicKey, deviceKeyPair.privateKey);
|
|
390
|
+
return decrypt(payload, deviceKeyPair.privateKey, peer.key, peer.shared);
|
|
372
391
|
};
|
|
373
392
|
exports.decryptEphemeral = decryptEphemeral;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - in-order seq delivers immediately (typing latency is the whole point of
|
|
11
11
|
* this path; nothing is buffered on the happy path)
|
|
12
|
-
* - a gap holds later keys for
|
|
12
|
+
* - a gap holds later keys for INPUT_HOLD_MS, then delivers what arrived anyway.
|
|
13
13
|
* Approximate order beats dropped keys: the relay is at-most-once, so a
|
|
14
14
|
* never-arriving seq must not wedge everything behind it forever.
|
|
15
15
|
* - a new epoch (sender restart) resets the high-water mark, and the old
|
|
@@ -48,7 +48,6 @@ export type SequencedInput = {
|
|
|
48
48
|
epoch: number;
|
|
49
49
|
};
|
|
50
50
|
export type InputSequencerOptions<T> = {
|
|
51
|
-
holdMs?: number;
|
|
52
51
|
schedule?: ScheduleFlush;
|
|
53
52
|
/** Called for each HELD message dropped without delivery — an epoch change
|
|
54
53
|
* or reset() sweeping the buffer. Accept-time drops are reported via the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-sequencer.d.ts","sourceRoot":"","sources":["../src/input-sequencer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,+DAA+D;AAC/D,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;sEAGsE;AACtE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;2DAI2D;AAC3D,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC;AAEtE,+BAA+B;AAC/B,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAE9B,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAC;AAExE,MAAM,MAAM,cAAc,GAAG;IACzB,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACnC,
|
|
1
|
+
{"version":3,"file":"input-sequencer.d.ts","sourceRoot":"","sources":["../src/input-sequencer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,+DAA+D;AAC/D,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;sEAGsE;AACtE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;2DAI2D;AAC3D,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC;AAEtE,+BAA+B;AAC/B,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAE9B,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAC;AAExE,MAAM,MAAM,cAAc,GAAG;IACzB,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACnC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;8EAE0E;IAC1E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,cAAc,IAAI;IACnD,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,YAAY,CAAC;IACjC;2BACuB;IACvB,KAAK,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AASF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,cAAc,EACzD,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EACzB,OAAM,qBAAqB,CAAC,CAAC,CAAM,KACpC,cAAc,CAAC,CAAC,CAkFlB,CAAC"}
|
package/dist/input-sequencer.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - in-order seq delivers immediately (typing latency is the whole point of
|
|
12
12
|
* this path; nothing is buffered on the happy path)
|
|
13
|
-
* - a gap holds later keys for
|
|
13
|
+
* - a gap holds later keys for INPUT_HOLD_MS, then delivers what arrived anyway.
|
|
14
14
|
* Approximate order beats dropped keys: the relay is at-most-once, so a
|
|
15
15
|
* never-arriving seq must not wedge everything behind it forever.
|
|
16
16
|
* - a new epoch (sender restart) resets the high-water mark, and the old
|
|
@@ -41,7 +41,6 @@ const defaultSchedule = (fn, ms) => {
|
|
|
41
41
|
return () => clearTimeout(timer);
|
|
42
42
|
};
|
|
43
43
|
const createInputSequencer = (deliver, opts = {}) => {
|
|
44
|
-
const holdMs = opts.holdMs ?? exports.INPUT_HOLD_MS;
|
|
45
44
|
const schedule = opts.schedule ?? defaultSchedule;
|
|
46
45
|
let epoch = null;
|
|
47
46
|
let highWater = 0;
|
|
@@ -112,7 +111,7 @@ const createInputSequencer = (deliver, opts = {}) => {
|
|
|
112
111
|
// One deadline per gap: it dates from when the hole opened, so a
|
|
113
112
|
// steady stream of later keys can't extend the wait indefinitely.
|
|
114
113
|
if (!cancelFlush)
|
|
115
|
-
cancelFlush = schedule(flush,
|
|
114
|
+
cancelFlush = schedule(flush, exports.INPUT_HOLD_MS);
|
|
116
115
|
return 'ok';
|
|
117
116
|
};
|
|
118
117
|
return {
|
package/dist/listener.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare const sessionsFingerprint: (sessions: AgentSession[]) => string;
|
|
|
56
56
|
* idle heartbeat is due. */
|
|
57
57
|
export declare const sessionsReportDue: (fingerprint: string, lastSentFingerprint: string | null, lastSentAtMs: number, nowMs: number) => boolean;
|
|
58
58
|
export declare const AUTH_FAILURE_CODES: ReadonlySet<number>;
|
|
59
|
-
export declare const checkRateLimit: (session: string, now?: number) => boolean;
|
|
59
|
+
export declare const checkRateLimit: (session: string, now?: number, cost?: number) => boolean;
|
|
60
60
|
/** Read the foreground command in the named tmux session's active pane. */
|
|
61
61
|
export declare const paneCurrentCommand: (session: string) => string | null;
|
|
62
62
|
/**
|
|
@@ -159,6 +159,22 @@ export interface ScreenSnapshot {
|
|
|
159
159
|
* token bucket as command injection.
|
|
160
160
|
*/
|
|
161
161
|
export declare const handleScreenRequest: (req: ScreenRequest) => ScreenSnapshot | null;
|
|
162
|
+
/**
|
|
163
|
+
* Translate the cursor into an index into the lines the frame carries.
|
|
164
|
+
*
|
|
165
|
+
* The visible pane is the tail of the capture, so the cursor's row counts back
|
|
166
|
+
* from the end — which also makes this correct after the byte cap has cut lines
|
|
167
|
+
* off the top. A cursor that falls outside what the frame carries returns null
|
|
168
|
+
* and simply is not drawn.
|
|
169
|
+
*/
|
|
170
|
+
export declare const cursorLineFor: (content: string, cursor: {
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
height: number;
|
|
174
|
+
}) => {
|
|
175
|
+
line: number;
|
|
176
|
+
col: number;
|
|
177
|
+
} | null;
|
|
162
178
|
/**
|
|
163
179
|
* After the phone injects keys, push fresh frames instead of waiting for its
|
|
164
180
|
* next screen request — the phone's post-tap re-pull raced the key landing
|
|
@@ -204,6 +220,10 @@ export interface FrameBudget {
|
|
|
204
220
|
* passed. False means this tick must skip WITHOUT marking the frame as sent —
|
|
205
221
|
* the diff-gate would otherwise swallow that content for good.
|
|
206
222
|
*/
|
|
223
|
+
/** Is there budget left this second? A peek, so a tick can decline to pay for a
|
|
224
|
+
* blocking capture it could not send anyway — spending the token here instead
|
|
225
|
+
* would burn budget on ticks the diff-gate goes on to skip. */
|
|
226
|
+
export declare const hasFrameBudget: (budget: FrameBudget, now: number) => boolean;
|
|
207
227
|
export declare const claimFrameSend: (budget: FrameBudget, now: number) => boolean;
|
|
208
228
|
export declare const STREAM_LEASE_MS = 15000;
|
|
209
229
|
export declare const MAX_CONCURRENT_STREAMS = 3;
|
|
@@ -220,6 +240,15 @@ export type StreamFramePayload = {
|
|
|
220
240
|
/** Stream incarnation (stats.startedAt) — lets the receiver reset its
|
|
221
241
|
* seq high-water mark when the daemon restarts the stream. */
|
|
222
242
|
epoch?: number;
|
|
243
|
+
/** Where to draw the cursor: a 0-based index into the lines this frame
|
|
244
|
+
* carries, and a 0-based CELL column (not a string index — a wide glyph
|
|
245
|
+
* spans two cells, which is what the viewer positions by). Absent when the
|
|
246
|
+
* cursor is outside the captured region. Deliberately outside the E2EE
|
|
247
|
+
* envelope, like sessionName and seq: a coordinate without the text it
|
|
248
|
+
* points into says nothing, and keeping it in the clear means the viewer
|
|
249
|
+
* can place the cursor before the pane content finishes decrypting. */
|
|
250
|
+
cursorLine?: number;
|
|
251
|
+
cursorCol?: number;
|
|
223
252
|
/** Plaintext pane content — only on unencrypted streams. */
|
|
224
253
|
content?: string;
|
|
225
254
|
/** E2EE envelope — replaces `content` on encrypted streams. */
|
|
@@ -257,7 +286,10 @@ export declare const isStreamCapReached: (activeCount: number) => boolean;
|
|
|
257
286
|
export declare const buildStreamFrame: (captured: {
|
|
258
287
|
content: string;
|
|
259
288
|
truncated: boolean;
|
|
260
|
-
}, sessionName: string, subscriberPublicKey?: string
|
|
289
|
+
}, sessionName: string, subscriberPublicKey?: string, cursor?: {
|
|
290
|
+
line: number;
|
|
291
|
+
col: number;
|
|
292
|
+
} | null) => Promise<StreamFramePayload | null>;
|
|
261
293
|
/**
|
|
262
294
|
* Handle agent.stream.start / agent.stream.stop. Returns true when the message
|
|
263
295
|
* was a stream-control message (so the caller skips the one-shot screen-peek
|
|
@@ -403,7 +435,7 @@ interface HandlePushDeps {
|
|
|
403
435
|
paneCommand?: (session: string) => string | null;
|
|
404
436
|
inject?: (session: string, text: string) => boolean;
|
|
405
437
|
sendKeys?: (session: string, tokens: string[]) => boolean;
|
|
406
|
-
rateLimit?: (session: string) => boolean;
|
|
438
|
+
rateLimit?: (session: string, now?: number, cost?: number) => boolean;
|
|
407
439
|
now?: () => number;
|
|
408
440
|
/** Injectable for tests; defaults to the REST-backed downloader. */
|
|
409
441
|
downloadAttachments?: (pushId: string, files: PushFileAttachment[]) => Promise<string[]>;
|
package/dist/listener.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAoBH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzG,OAAO,EAAiD,KAAK,UAAU,EAA4C,MAAM,kBAAkB,CAAC;AAE5I,OAAO,EAA4E,KAAK,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACvI,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAmCtG,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,YAAY,EAAE,KAAG,MAS7C,CAAC;AAEnB;6BAC6B;AAC7B,eAAO,MAAM,iBAAiB,GAC1B,aAAa,MAAM,EACnB,qBAAqB,MAAM,GAAG,IAAI,EAClC,cAAc,MAAM,EACpB,OAAO,MAAM,KACd,OAEoD,CAAC;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
28
|
+
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.scheduleInjectSnapshots = exports.cursorLineFor = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
30
30
|
const crypto_1 = require("crypto");
|
|
31
31
|
const fs_1 = require("fs");
|
|
@@ -92,10 +92,19 @@ exports.sessionsFingerprint = sessionsFingerprint;
|
|
|
92
92
|
const sessionsReportDue = (fingerprint, lastSentFingerprint, lastSentAtMs, nowMs) => fingerprint !== lastSentFingerprint ||
|
|
93
93
|
nowMs - lastSentAtMs >= exports.SESSION_REPORT_HEARTBEAT_MS;
|
|
94
94
|
exports.sessionsReportDue = sessionsReportDue;
|
|
95
|
-
// Per-session token bucket — caps a runaway/compromised sender.
|
|
96
|
-
//
|
|
97
|
-
|
|
95
|
+
// Per-session token bucket — caps a runaway/compromised sender.
|
|
96
|
+
//
|
|
97
|
+
// Weighted rather than flat, because the two things that pass through it are
|
|
98
|
+
// not the same size. A command submit is one whole instruction to the agent;
|
|
99
|
+
// 30 a minute has always been the ceiling for that, and it stays exactly that
|
|
100
|
+
// (120 / SUBMIT_COST). A named key is a keystroke — arrowing through a menu or
|
|
101
|
+
// holding Backspace is ordinary human speed, and charging it as if it were an
|
|
102
|
+
// instruction made the phone's key row refuse a normal burst of taps after a
|
|
103
|
+
// couple of seconds.
|
|
104
|
+
const RATE_LIMIT_TOKENS = 120;
|
|
98
105
|
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
106
|
+
/** What one command submit costs, keeping its ceiling at the old 30/min. */
|
|
107
|
+
const SUBMIT_COST = 4;
|
|
99
108
|
// Shells are refused: a shell prompt + send-keys = arbitrary command exec.
|
|
100
109
|
const SHELL_COMMANDS = new Set(['bash', 'zsh', 'fish', 'sh', 'dash', 'ksh', 'tcsh', 'csh', 'pwsh']);
|
|
101
110
|
// Auth-failure close codes: retrying with the same bad credentials hammers
|
|
@@ -112,18 +121,18 @@ const pruneStaleBuckets = (now) => {
|
|
|
112
121
|
buckets.delete(key);
|
|
113
122
|
}
|
|
114
123
|
};
|
|
115
|
-
const checkRateLimit = (session, now = Date.now()) => {
|
|
124
|
+
const checkRateLimit = (session, now = Date.now(), cost = 1) => {
|
|
116
125
|
pruneStaleBuckets(now);
|
|
117
126
|
const b = buckets.get(session) ?? { tokens: RATE_LIMIT_TOKENS, lastRefillAt: now };
|
|
118
127
|
const elapsed = Math.max(0, now - b.lastRefillAt);
|
|
119
128
|
// Fractional refill is intentional: smooths the boundary so a session
|
|
120
129
|
// hitting the cap doesn't have to wait a full window for the next slot.
|
|
121
130
|
const refilled = Math.min(RATE_LIMIT_TOKENS, b.tokens + (elapsed / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_TOKENS);
|
|
122
|
-
if (refilled <
|
|
131
|
+
if (refilled < cost) {
|
|
123
132
|
buckets.set(session, { tokens: refilled, lastRefillAt: now });
|
|
124
133
|
return false;
|
|
125
134
|
}
|
|
126
|
-
buckets.set(session, { tokens: refilled -
|
|
135
|
+
buckets.set(session, { tokens: refilled - cost, lastRefillAt: now });
|
|
127
136
|
return true;
|
|
128
137
|
};
|
|
129
138
|
exports.checkRateLimit = checkRateLimit;
|
|
@@ -143,14 +152,53 @@ const isShellPane = (command) => {
|
|
|
143
152
|
return false;
|
|
144
153
|
return SHELL_COMMANDS.has(command);
|
|
145
154
|
};
|
|
155
|
+
/** Where a message is parked between `set-buffer` and `paste-buffer`. Named,
|
|
156
|
+
* so the user's own paste stack is never pushed onto, and `-d` drops it
|
|
157
|
+
* again the moment it has been delivered. */
|
|
158
|
+
const INJECT_BUFFER = 'zeph-inject';
|
|
159
|
+
const sendLiteral = (session, text) => (0, child_process_1.spawnSync)('tmux', tmuxArgs(['send-keys', '-l', '-t', session, text]), { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
160
|
+
.status === 0;
|
|
161
|
+
/**
|
|
162
|
+
* Put the message in the pane as a PASTE rather than as typing.
|
|
163
|
+
*
|
|
164
|
+
* `send-keys -l` hands the whole string to the application as if it had been
|
|
165
|
+
* typed at once, and a TUI that re-renders per character has to keep up with a
|
|
166
|
+
* burst it never sees from a human. With Hangul — three bytes and two columns
|
|
167
|
+
* per character — it does not: characters go missing and the wrapped line is
|
|
168
|
+
* painted twice. A phone message is a paste, so send it as one.
|
|
169
|
+
*
|
|
170
|
+
* `paste-buffer -p` brackets it only when the application has actually asked
|
|
171
|
+
* for bracketed paste (DECSET 2004); against one that has not, tmux sends the
|
|
172
|
+
* plain text and nothing changes. That gating is why this is safe to do
|
|
173
|
+
* unconditionally — the markers can never leak into an app that would show
|
|
174
|
+
* them as literal `[200~`.
|
|
175
|
+
*
|
|
176
|
+
* Falls back to the old path if either tmux call fails, because a message
|
|
177
|
+
* delivered imperfectly still beats one not delivered.
|
|
178
|
+
*/
|
|
179
|
+
const pasteText = (session, text) => {
|
|
180
|
+
// `--` so a message that begins with a dash is data, not an option. The
|
|
181
|
+
// buffer contents are data to paste-buffer as well, which preserves the
|
|
182
|
+
// property `send-keys -l` had: no escape sequence inside a message can
|
|
183
|
+
// drive another tmux command.
|
|
184
|
+
const set = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['set-buffer', '-b', INJECT_BUFFER, '--', text]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
185
|
+
if (set.status !== 0)
|
|
186
|
+
return sendLiteral(session, text);
|
|
187
|
+
const paste = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['paste-buffer', '-d', '-p', '-b', INJECT_BUFFER, '-t', session]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
188
|
+
if (paste.status === 0)
|
|
189
|
+
return true;
|
|
190
|
+
// `-d` never ran, so the buffer is still there holding the message.
|
|
191
|
+
(0, child_process_1.spawnSync)('tmux', tmuxArgs(['delete-buffer', '-b', INJECT_BUFFER]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
192
|
+
return sendLiteral(session, text);
|
|
193
|
+
};
|
|
146
194
|
/**
|
|
147
|
-
* Inject text into a tmux session:
|
|
148
|
-
*
|
|
149
|
-
*
|
|
195
|
+
* Inject text into a tmux session: the message as a paste, then a separate
|
|
196
|
+
* `Enter` to submit it. The two stay separate because a bracketed paste is
|
|
197
|
+
* text and only text — newlines inside it must not submit early, and the
|
|
198
|
+
* submit has to be a real key press.
|
|
150
199
|
*/
|
|
151
200
|
const injectKeys = (session, text) => {
|
|
152
|
-
|
|
153
|
-
if (a.status !== 0)
|
|
201
|
+
if (!pasteText(session, text))
|
|
154
202
|
return false;
|
|
155
203
|
const b = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['send-keys', '-t', session, 'Enter']), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
156
204
|
return b.status === 0;
|
|
@@ -728,6 +776,47 @@ const capturePane = (sessionName, escapes = false, lines = SCREEN_PEEK_LINES) =>
|
|
|
728
776
|
}
|
|
729
777
|
return { content, truncated };
|
|
730
778
|
};
|
|
779
|
+
/**
|
|
780
|
+
* Where the cursor sits, in the coordinates of a captured frame.
|
|
781
|
+
*
|
|
782
|
+
* `capture-pane` gives cells and colors but no cursor, so the mirror has never
|
|
783
|
+
* drawn one — which makes an arrow key indistinguishable from a dropped one.
|
|
784
|
+
* tmux reports the position separately; `pane_height` is what turns a
|
|
785
|
+
* pane-relative row into a row of the captured text, since the capture reaches
|
|
786
|
+
* back into scrollback and the visible pane is only its last `pane_height`
|
|
787
|
+
* lines.
|
|
788
|
+
*
|
|
789
|
+
* Read only for frames that are actually about to be sent — one extra tmux
|
|
790
|
+
* spawn per SENT frame rather than per tick.
|
|
791
|
+
*/
|
|
792
|
+
const capturePaneCursor = (sessionName) => {
|
|
793
|
+
const r = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['display-message', '-p', '-t', sessionName, '#{cursor_x},#{cursor_y},#{pane_height}']), { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
794
|
+
if (r.status !== 0)
|
|
795
|
+
return null;
|
|
796
|
+
const [x, y, height] = (r.stdout ?? '').trim().split(',').map(Number);
|
|
797
|
+
if (![x, y, height].every((n) => Number.isSafeInteger(n) && n >= 0))
|
|
798
|
+
return null;
|
|
799
|
+
return { x, y, height };
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* Translate the cursor into an index into the lines the frame carries.
|
|
803
|
+
*
|
|
804
|
+
* The visible pane is the tail of the capture, so the cursor's row counts back
|
|
805
|
+
* from the end — which also makes this correct after the byte cap has cut lines
|
|
806
|
+
* off the top. A cursor that falls outside what the frame carries returns null
|
|
807
|
+
* and simply is not drawn.
|
|
808
|
+
*/
|
|
809
|
+
const cursorLineFor = (content, cursor) => {
|
|
810
|
+
// capture-pane ends the last row with a newline, which split() turns into a
|
|
811
|
+
// trailing empty element that is not a pane row.
|
|
812
|
+
const lines = content.split('\n');
|
|
813
|
+
const rows = lines.length > 0 && lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
814
|
+
const line = rows - cursor.height + cursor.y;
|
|
815
|
+
if (line < 0 || line >= rows)
|
|
816
|
+
return null;
|
|
817
|
+
return { line, col: cursor.x };
|
|
818
|
+
};
|
|
819
|
+
exports.cursorLineFor = cursorLineFor;
|
|
731
820
|
/** Delays after a key injection at which the pane is re-captured and pushed
|
|
732
821
|
* to the phone unsolicited. The first frame lands right after most TUI
|
|
733
822
|
* redraws; the second catches slow animations and is skipped when nothing
|
|
@@ -744,6 +833,13 @@ const pendingInjectSnapshots = new Map();
|
|
|
744
833
|
* captures so a burst of arrow presses yields frames after the LAST press.
|
|
745
834
|
*/
|
|
746
835
|
const scheduleInjectSnapshots = (sessionName, send, delays = INJECT_SNAPSHOT_DELAYS_MS) => {
|
|
836
|
+
// These snapshots exist to show the phone what a keystroke did when nobody
|
|
837
|
+
// is mirroring the pane. A live stream already repaints it at the burst
|
|
838
|
+
// cadence, so scheduling them there is two extra blocking captures and two
|
|
839
|
+
// extra sends per keystroke — outside the stream's own send budget, which
|
|
840
|
+
// is exactly the cost the budget exists to bound.
|
|
841
|
+
if (activeStreams.has(sessionName))
|
|
842
|
+
return;
|
|
747
843
|
for (const t of pendingInjectSnapshots.get(sessionName) ?? [])
|
|
748
844
|
clearTimeout(t);
|
|
749
845
|
let lastContent = null;
|
|
@@ -797,6 +893,13 @@ exports.streamCadence = streamCadence;
|
|
|
797
893
|
* passed. False means this tick must skip WITHOUT marking the frame as sent —
|
|
798
894
|
* the diff-gate would otherwise swallow that content for good.
|
|
799
895
|
*/
|
|
896
|
+
/** Is there budget left this second? A peek, so a tick can decline to pay for a
|
|
897
|
+
* blocking capture it could not send anyway — spending the token here instead
|
|
898
|
+
* would burn budget on ticks the diff-gate goes on to skip. */
|
|
899
|
+
const hasFrameBudget = (budget, now) => now - budget.windowStartedAt >= 1_000 || now < budget.windowStartedAt
|
|
900
|
+
? true
|
|
901
|
+
: budget.sent < exports.MAX_FRAMES_PER_SEC;
|
|
902
|
+
exports.hasFrameBudget = hasFrameBudget;
|
|
800
903
|
const claimFrameSend = (budget, now) => {
|
|
801
904
|
// `now < windowStartedAt` = the clock ran backwards (NTP step, resume).
|
|
802
905
|
// Without the guard the window never rolls again and a spent budget
|
|
@@ -970,12 +1073,13 @@ echo) => {
|
|
|
970
1073
|
* (bad key, crypto not ready on the first tick) returns null so the caller
|
|
971
1074
|
* drops the frame — an encrypted stream never leaks a plaintext frame.
|
|
972
1075
|
*/
|
|
973
|
-
const buildStreamFrame = async (captured, sessionName, subscriberPublicKey) => {
|
|
1076
|
+
const buildStreamFrame = async (captured, sessionName, subscriberPublicKey, cursor) => {
|
|
974
1077
|
const base = {
|
|
975
1078
|
subtype: 'agent.stream.frame',
|
|
976
1079
|
sessionName,
|
|
977
1080
|
capturedAt: new Date().toISOString(),
|
|
978
1081
|
truncated: captured.truncated,
|
|
1082
|
+
...(cursor ? { cursorLine: cursor.line, cursorCol: cursor.col } : {}),
|
|
979
1083
|
};
|
|
980
1084
|
if (!subscriberPublicKey)
|
|
981
1085
|
return { ...base, content: captured.content };
|
|
@@ -1054,6 +1158,12 @@ const handleStreamControl = (req, send) => {
|
|
|
1054
1158
|
return true;
|
|
1055
1159
|
}
|
|
1056
1160
|
let lastContent = null;
|
|
1161
|
+
// The cursor's last reported place, as `line,col`. Part of the diff-gate
|
|
1162
|
+
// because moving the cursor is a change the CAPTURE cannot show: an arrow
|
|
1163
|
+
// key inside a prompt leaves every character where it was, so gating on the
|
|
1164
|
+
// text alone means the mirror's cursor never moves — which is precisely the
|
|
1165
|
+
// feedback the cursor exists to give.
|
|
1166
|
+
let lastCursor = null;
|
|
1057
1167
|
const stats = {
|
|
1058
1168
|
startedAt: Date.now(),
|
|
1059
1169
|
frames: 0,
|
|
@@ -1104,8 +1214,28 @@ const handleStreamControl = (req, send) => {
|
|
|
1104
1214
|
(0, exports.stopStream)(sessionName);
|
|
1105
1215
|
return false;
|
|
1106
1216
|
}
|
|
1217
|
+
// capturePane is a blocking tmux spawn. Asking the budget first means a
|
|
1218
|
+
// second that is already full costs nothing rather than paying for a
|
|
1219
|
+
// capture whose frame could not go out — a peek, not a claim, so a tick
|
|
1220
|
+
// the diff-gate goes on to skip does not spend the token.
|
|
1221
|
+
if (!(0, exports.hasFrameBudget)(budget, Date.now())) {
|
|
1222
|
+
stats.skipped++;
|
|
1223
|
+
stats.rateCapped++;
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1107
1226
|
const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
|
|
1108
|
-
if (!captured
|
|
1227
|
+
if (!captured) {
|
|
1228
|
+
stats.skipped++;
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
// Read per tick, not per sent frame: the gate below has to know whether
|
|
1232
|
+
// the cursor moved, and by the time a frame is being built the tick
|
|
1233
|
+
// that would have carried the move has already been skipped. One extra
|
|
1234
|
+
// tmux spawn on ticks that go on to send nothing is what that costs.
|
|
1235
|
+
const cursorAt = capturePaneCursor(sessionName);
|
|
1236
|
+
const cursor = cursorAt ? (0, exports.cursorLineFor)(captured.content, cursorAt) : null;
|
|
1237
|
+
const cursorKey = cursor ? `${cursor.line},${cursor.col}` : '';
|
|
1238
|
+
if (captured.content === lastContent && cursorKey === lastCursor) {
|
|
1109
1239
|
stats.skipped++;
|
|
1110
1240
|
return true; // diff-gate
|
|
1111
1241
|
}
|
|
@@ -1124,12 +1254,13 @@ const handleStreamControl = (req, send) => {
|
|
|
1124
1254
|
// fps by one frame per episode in the R2 numbers.
|
|
1125
1255
|
const inBurst = armedDelay === exports.BURST_INTERVAL_MS;
|
|
1126
1256
|
lastContent = captured.content;
|
|
1257
|
+
lastCursor = cursorKey;
|
|
1127
1258
|
// Stamp the sequence in CAPTURE order, synchronously — frame assembly
|
|
1128
1259
|
// is async (encryption) and fire-and-forget, so resolve order is not
|
|
1129
1260
|
// guaranteed under load; the receiver drops any seq it has already
|
|
1130
1261
|
// painted past.
|
|
1131
1262
|
const seq = ++wireSeq;
|
|
1132
|
-
void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey).then((frame) => {
|
|
1263
|
+
void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey, cursor).then((frame) => {
|
|
1133
1264
|
// Stream stopped or restarted while this frame was in flight —
|
|
1134
1265
|
// `stats` is unique per start, so it doubles as the identity
|
|
1135
1266
|
// token. Don't send into a dead/replaced stream or skew its stats.
|
|
@@ -1141,6 +1272,7 @@ const handleStreamControl = (req, send) => {
|
|
|
1141
1272
|
// subscriber key) never recovers: fail closed after a few
|
|
1142
1273
|
// strikes instead of retrying every tick for 5 minutes.
|
|
1143
1274
|
lastContent = null;
|
|
1275
|
+
lastCursor = null;
|
|
1144
1276
|
// Init still in flight (or failed — its own path fail-closes):
|
|
1145
1277
|
// a not-yet-ready key is not a malformed key, don't strike.
|
|
1146
1278
|
if ((0, crypto_js_1.getDevicePublicKey)() === null)
|
|
@@ -1356,18 +1488,14 @@ const enqueueInput = (input, send) => {
|
|
|
1356
1488
|
// would let the higher epoch supersede the other and silence it.
|
|
1357
1489
|
const senderKey = input.deviceId ? `${input.sessionName}#${input.deviceId}` : input.sessionName;
|
|
1358
1490
|
const sequencer = inputSequencerFor(senderKey);
|
|
1359
|
-
if (!sequencer)
|
|
1360
|
-
|
|
1361
|
-
send(streamErrorFrame(input.sessionName, 'input_rejected', input));
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1491
|
+
if (!sequencer)
|
|
1492
|
+
return refuseInput(input, send, 'sender lane cap reached');
|
|
1364
1493
|
const accepted = sequencer.accept({ ...input, send });
|
|
1365
1494
|
if (accepted !== 'ok') {
|
|
1366
1495
|
// overflow / superseded / stale — the message was dropped, and the
|
|
1367
1496
|
// sender must hear so instead of waiting on a keystroke that never
|
|
1368
1497
|
// lands (the sequencer reports swept HELD messages via onDiscard).
|
|
1369
|
-
|
|
1370
|
-
send(streamErrorFrame(input.sessionName, 'input_rejected', input));
|
|
1498
|
+
refuseInput(input, send, accepted);
|
|
1371
1499
|
}
|
|
1372
1500
|
};
|
|
1373
1501
|
/**
|
|
@@ -1399,17 +1527,21 @@ exports.pendingInputDecrypts = pendingInputDecrypts;
|
|
|
1399
1527
|
* plaintext path runs — the envelope hides the payload from the relay, never
|
|
1400
1528
|
* from the whitelist or the caps.
|
|
1401
1529
|
*/
|
|
1530
|
+
/**
|
|
1531
|
+
* Refuse one input message: log why here, tell the sender only THAT it was
|
|
1532
|
+
* refused. The reason is derived from plaintext the relay must not learn, so it
|
|
1533
|
+
* never rides the frame; a message with no sessionName gets no frame at all,
|
|
1534
|
+
* since the sender would have nothing to match it against.
|
|
1535
|
+
*/
|
|
1536
|
+
const refuseInput = (msg, send, reason) => {
|
|
1537
|
+
log(`! input ${msg.sessionName ?? '(no session)'}: ${reason} — drop`);
|
|
1538
|
+
if (typeof msg.sessionName === 'string' && msg.sessionName) {
|
|
1539
|
+
send(streamErrorFrame(msg.sessionName, 'input_rejected', msg));
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1402
1542
|
const handleEncryptedInput = (msg, send) => {
|
|
1403
1543
|
const { sessionName } = msg;
|
|
1404
|
-
const refuse = (reason) =>
|
|
1405
|
-
log(`! input ${sessionName ?? '(no session)'}: ${reason} — drop`);
|
|
1406
|
-
// No sessionName means the sender can't match the error to anything.
|
|
1407
|
-
// The reason never rides along: it is derived from plaintext the relay
|
|
1408
|
-
// must not learn, so it stays in this host's log.
|
|
1409
|
-
if (typeof sessionName === 'string' && sessionName) {
|
|
1410
|
-
send(streamErrorFrame(sessionName, 'input_rejected', msg));
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1544
|
+
const refuse = (reason) => refuseInput(msg, send, reason);
|
|
1413
1545
|
if (typeof sessionName !== 'string' || !sessionName)
|
|
1414
1546
|
return refuse('no sessionName');
|
|
1415
1547
|
const stream = activeStreams.get(sessionName);
|
|
@@ -1467,11 +1599,10 @@ const handleEncryptedInput = (msg, send) => {
|
|
|
1467
1599
|
// A real envelope clears the strikes: the cap is there to stop a flood,
|
|
1468
1600
|
// not to retire a stream that saw one corrupted message.
|
|
1469
1601
|
const live = activeStreams.get(sessionName);
|
|
1470
|
-
if (live?.stats
|
|
1471
|
-
live.inputDecryptFailures = 0;
|
|
1472
|
-
if (activeStreams.get(sessionName)?.stats !== incarnation) {
|
|
1602
|
+
if (live?.stats !== incarnation) {
|
|
1473
1603
|
return refuse('stream replaced while decrypting');
|
|
1474
1604
|
}
|
|
1605
|
+
live.inputDecryptFailures = 0;
|
|
1475
1606
|
const payload = parseSealedInput(opened);
|
|
1476
1607
|
if (!payload)
|
|
1477
1608
|
return refuse('sealed payload is not an input object');
|
|
@@ -1522,11 +1653,7 @@ const handleCommandInput = (msg, send) => {
|
|
|
1522
1653
|
}
|
|
1523
1654
|
const checked = (0, exports.validateInputMessage)(msg);
|
|
1524
1655
|
if (!checked.ok) {
|
|
1525
|
-
|
|
1526
|
-
// No sessionName means the sender can't match the error to anything.
|
|
1527
|
-
if (typeof msg.sessionName === 'string' && msg.sessionName) {
|
|
1528
|
-
send(streamErrorFrame(msg.sessionName, 'input_rejected', msg));
|
|
1529
|
-
}
|
|
1656
|
+
refuseInput(msg, send, checked.reason);
|
|
1530
1657
|
return true;
|
|
1531
1658
|
}
|
|
1532
1659
|
const { input } = checked;
|
|
@@ -1535,8 +1662,7 @@ const handleCommandInput = (msg, send) => {
|
|
|
1535
1662
|
// against. The sender learns in one hop and falls back to a REST push.
|
|
1536
1663
|
const stream = activeStreams.get(input.sessionName);
|
|
1537
1664
|
if (!stream) {
|
|
1538
|
-
|
|
1539
|
-
send(streamErrorFrame(input.sessionName, 'input_rejected', input));
|
|
1665
|
+
refuseInput(input, send, 'no live stream');
|
|
1540
1666
|
return true;
|
|
1541
1667
|
}
|
|
1542
1668
|
// This stream's outbound half is E2EE for that subscriber, so accepting a
|
|
@@ -1544,8 +1670,7 @@ const handleCommandInput = (msg, send) => {
|
|
|
1544
1670
|
// and nothing about it proves the subscriber sent it. An encrypted stream
|
|
1545
1671
|
// takes sealed input (above) or none.
|
|
1546
1672
|
if (stream.subscriberPublicKey) {
|
|
1547
|
-
|
|
1548
|
-
send(streamErrorFrame(input.sessionName, 'input_rejected', input));
|
|
1673
|
+
refuseInput(input, send, 'stream is E2EE, plaintext input refused');
|
|
1549
1674
|
return true;
|
|
1550
1675
|
}
|
|
1551
1676
|
enqueueInput(input, send);
|
|
@@ -1639,11 +1764,11 @@ exports.collectSessions = collectSessions;
|
|
|
1639
1764
|
* the structured `agent.command` push type and the legacy `@<session>`
|
|
1640
1765
|
* prefix path route through here so the defense layers can't diverge.
|
|
1641
1766
|
*/
|
|
1642
|
-
const passesInjectGuards = (session, deps) => {
|
|
1767
|
+
const passesInjectGuards = (session, deps, cost = 1) => {
|
|
1643
1768
|
// Rate bucket first: the pane probe below is a blocking tmux spawnSync,
|
|
1644
1769
|
// and the sequencer can flush several held messages back-to-back — an
|
|
1645
1770
|
// empty bucket must refuse before paying that probe N times, not after.
|
|
1646
|
-
if (!(deps.rateLimit ?? exports.checkRateLimit)(session)) {
|
|
1771
|
+
if (!(deps.rateLimit ?? exports.checkRateLimit)(session, Date.now(), cost)) {
|
|
1647
1772
|
log(`! ${session}: rate-limited — drop`);
|
|
1648
1773
|
return false;
|
|
1649
1774
|
}
|
|
@@ -1686,7 +1811,7 @@ const tryInject = (session, text, deps) => {
|
|
|
1686
1811
|
log(`! ${session}: empty text — drop`);
|
|
1687
1812
|
return false;
|
|
1688
1813
|
}
|
|
1689
|
-
if (!passesInjectGuards(session, deps))
|
|
1814
|
+
if (!passesInjectGuards(session, deps, SUBMIT_COST))
|
|
1690
1815
|
return false;
|
|
1691
1816
|
const ok = (deps.inject ?? injectKeys)(session, text);
|
|
1692
1817
|
const preview = text.length > 60 ? text.slice(0, 60) + '…' : text;
|