litmus-cli 1.4.70 → 1.4.72-capturetest.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/README.md +52 -9
- package/dist/commands/connect.d.ts +33 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +119 -45
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/forward.d.ts +7 -16
- package/dist/commands/forward.d.ts.map +1 -1
- package/dist/commands/forward.js +97 -75
- package/dist/commands/forward.js.map +1 -1
- package/dist/commands/ssh.d.ts +3 -0
- package/dist/commands/ssh.d.ts.map +1 -0
- package/dist/commands/ssh.js +50 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +7 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +38 -8
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auto-forward.d.ts +31 -0
- package/dist/lib/auto-forward.d.ts.map +1 -0
- package/dist/lib/auto-forward.js +258 -0
- package/dist/lib/auto-forward.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/connection-attempt.d.ts +13 -2
- package/dist/lib/connection-attempt.d.ts.map +1 -1
- package/dist/lib/connection-attempt.js +16 -3
- package/dist/lib/connection-attempt.js.map +1 -1
- package/dist/lib/cursor-session.d.ts +6 -0
- package/dist/lib/cursor-session.d.ts.map +1 -1
- package/dist/lib/cursor-session.js +21 -1
- package/dist/lib/cursor-session.js.map +1 -1
- package/dist/lib/environment-checks.d.ts +1 -2
- package/dist/lib/environment-checks.d.ts.map +1 -1
- package/dist/lib/environment-checks.js +4 -9
- package/dist/lib/environment-checks.js.map +1 -1
- package/dist/lib/hook-logger.cjs +79 -22
- package/dist/lib/hosted-capture.d.ts +21 -0
- package/dist/lib/hosted-capture.d.ts.map +1 -0
- package/dist/lib/hosted-capture.js +229 -0
- package/dist/lib/hosted-capture.js.map +1 -0
- package/dist/lib/hosted-watcher.d.ts +2 -0
- package/dist/lib/hosted-watcher.d.ts.map +1 -0
- package/dist/lib/hosted-watcher.js +176 -0
- package/dist/lib/hosted-watcher.js.map +1 -0
- package/dist/lib/install-ssh-forward-hook.d.ts +6 -0
- package/dist/lib/install-ssh-forward-hook.d.ts.map +1 -0
- package/dist/lib/install-ssh-forward-hook.js +57 -0
- package/dist/lib/install-ssh-forward-hook.js.map +1 -0
- package/dist/lib/port-forward.d.ts +16 -0
- package/dist/lib/port-forward.d.ts.map +1 -0
- package/dist/lib/port-forward.js +46 -0
- package/dist/lib/port-forward.js.map +1 -0
- package/dist/lib/session-state.d.ts +61 -0
- package/dist/lib/session-state.d.ts.map +1 -0
- package/dist/lib/session-state.js +92 -0
- package/dist/lib/session-state.js.map +1 -0
- package/dist/lib/ssh-client.d.ts +11 -2
- package/dist/lib/ssh-client.d.ts.map +1 -1
- package/dist/lib/ssh-client.js +66 -7
- package/dist/lib/ssh-client.js.map +1 -1
- package/dist/lib/ssh-forward-hook.d.ts +13 -0
- package/dist/lib/ssh-forward-hook.d.ts.map +1 -0
- package/dist/lib/ssh-forward-hook.js +93 -0
- package/dist/lib/ssh-forward-hook.js.map +1 -0
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +7 -2
- package/dist/lib/tracker.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Laptop-side capture supervisor. No file snapshots or auto-submission. */
|
|
2
|
+
import { CLI_VERSION } from "./version.js";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, openSync, readSync, closeSync, fstatSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { atomicPrivateJson, captureLease, HOSTED_CAPTURE_FILE, acquireCaptureSetupLock } from "./hosted-capture.js";
|
|
6
|
+
import { retireAiCaptureForEndedSession, sessionOverMarked, writeSessionOverMarker } from "./session-over.js";
|
|
7
|
+
const projectDir = process.argv[2];
|
|
8
|
+
const activityLog = process.argv[3];
|
|
9
|
+
const nonce = process.argv[5];
|
|
10
|
+
const stateDir = path.join(projectDir, ".litmus");
|
|
11
|
+
const config = JSON.parse(readFileSync(path.join(stateDir, "config.json"), "utf8"));
|
|
12
|
+
const ready = path.join(stateDir, "tracker.ready");
|
|
13
|
+
const pidFile = path.join(stateDir, "tracker.pid");
|
|
14
|
+
let stopped = false;
|
|
15
|
+
let lastFlushAttempt = 0;
|
|
16
|
+
let offset = 0;
|
|
17
|
+
try {
|
|
18
|
+
offset = Number(readFileSync(path.join(stateDir, "upload-offset"), "utf8")) || 0;
|
|
19
|
+
}
|
|
20
|
+
catch { /* first boot */ }
|
|
21
|
+
function ownsRegistration() {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(HOSTED_CAPTURE_FILE, "utf8")).projectDir === projectDir;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function stop(retire) {
|
|
30
|
+
if (stopped)
|
|
31
|
+
return await new Promise(() => { });
|
|
32
|
+
stopped = true;
|
|
33
|
+
let ownsPid = false;
|
|
34
|
+
try {
|
|
35
|
+
ownsPid = readFileSync(pidFile, "utf8").trim() === String(process.pid);
|
|
36
|
+
}
|
|
37
|
+
catch { /* absent */ }
|
|
38
|
+
if (ownsRegistration() && ownsPid) {
|
|
39
|
+
// Silence the hooks BEFORE slow cleanup; do not touch a successor window.
|
|
40
|
+
const registration = JSON.parse(readFileSync(HOSTED_CAPTURE_FILE, "utf8"));
|
|
41
|
+
atomicPrivateJson(HOSTED_CAPTURE_FILE, { ...registration, expiresAt: 0 });
|
|
42
|
+
if (retire) {
|
|
43
|
+
// Disable collection first. Bounded draining cannot start new capture.
|
|
44
|
+
const deadline = Date.now() + 10000;
|
|
45
|
+
try {
|
|
46
|
+
while (Date.now() < deadline && await flush()) { /* next bounded batch */ }
|
|
47
|
+
}
|
|
48
|
+
catch { /* preserve local outbox on failure */ }
|
|
49
|
+
writeSessionOverMarker(projectDir, config.token, "coding session ended");
|
|
50
|
+
const cleanupDeadline = Date.now() + 10000;
|
|
51
|
+
let cleaned = false;
|
|
52
|
+
while (!cleaned && Date.now() < cleanupDeadline) {
|
|
53
|
+
let releaseSetup;
|
|
54
|
+
try {
|
|
55
|
+
// Setup or an unrelated port owner may briefly hold the guard. Keep
|
|
56
|
+
// capture disabled while retrying; never uninstall a successor's hooks.
|
|
57
|
+
releaseSetup = await acquireCaptureSetupLock(`${HOSTED_CAPTURE_FILE}.lock`);
|
|
58
|
+
retireAiCaptureForEndedSession(projectDir, config.token);
|
|
59
|
+
if (ownsRegistration())
|
|
60
|
+
rmSync(HOSTED_CAPTURE_FILE, { force: true });
|
|
61
|
+
cleaned = true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (releaseSetup)
|
|
68
|
+
await releaseSetup();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// On persistent contention, the expired routing record, ended marker and
|
|
72
|
+
// registry entry preserve pending cleanup for the next connect attempt.
|
|
73
|
+
// Credentials are no longer needed to release that retired installation.
|
|
74
|
+
rmSync(path.join(stateDir, "config.json"), { force: true });
|
|
75
|
+
if (!cleaned)
|
|
76
|
+
process.stderr.write("[hosted capture] capture stopped; hook cleanup pending until the local setup guard is available on reconnect\n");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
if (readFileSync(ready, "utf8").trim() === String(process.pid))
|
|
81
|
+
rmSync(ready, { force: true });
|
|
82
|
+
}
|
|
83
|
+
catch { /* absent */ }
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
process.on("SIGTERM", () => { void stop(false); });
|
|
87
|
+
process.on("SIGINT", () => { void stop(false); });
|
|
88
|
+
async function flush() {
|
|
89
|
+
if (!existsSync(activityLog))
|
|
90
|
+
return false;
|
|
91
|
+
const fd = openSync(activityLog, "r");
|
|
92
|
+
let bytes;
|
|
93
|
+
try {
|
|
94
|
+
if (offset > fstatSync(fd).size)
|
|
95
|
+
offset = 0;
|
|
96
|
+
const buffer = Buffer.alloc(8 * 1024 * 1024);
|
|
97
|
+
bytes = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, offset));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
// A bounded batch; only complete lines, no prompt bodies in diagnostics.
|
|
103
|
+
let end = -1;
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
106
|
+
if (bytes[i] === 10) {
|
|
107
|
+
end = i;
|
|
108
|
+
if (++count === 512)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (end < 0)
|
|
113
|
+
return false;
|
|
114
|
+
const lines = bytes.subarray(0, end).toString("utf8").split("\n").filter(Boolean);
|
|
115
|
+
const events = lines.map(line => JSON.parse(line));
|
|
116
|
+
const result = await fetch(`${config.backendUrl}/cli/activity`, {
|
|
117
|
+
method: "POST", redirect: "error", headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json", "X-Litmus-CLI-Version": CLI_VERSION, "X-Litmus-Hosted-Capture": "v1" },
|
|
118
|
+
body: JSON.stringify({ events }), signal: AbortSignal.timeout(4000),
|
|
119
|
+
});
|
|
120
|
+
// Never discard a refused or unknown-outcome batch. Hook uploads share event IDs.
|
|
121
|
+
const acknowledgement = result.ok ? await result.json() : null;
|
|
122
|
+
const acknowledged = acknowledgement?.success === true;
|
|
123
|
+
if (acknowledged && typeof acknowledgement?.discarded === "number" && acknowledgement.discarded > 0) {
|
|
124
|
+
process.stderr.write(`[hosted capture] ${acknowledgement.discarded} events permanently rejected outside their capture authorization window; original outbox retained\n`);
|
|
125
|
+
}
|
|
126
|
+
if (acknowledged) {
|
|
127
|
+
offset += end + 1;
|
|
128
|
+
writeFileSync(path.join(stateDir, "upload-offset"), String(offset), { mode: 0o600 });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
async function run() {
|
|
134
|
+
// Parent stamps tracker.pid during spawn. Yield once before asserting ownership.
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
136
|
+
if (!ownsRegistration() || sessionOverMarked(projectDir))
|
|
137
|
+
await stop(true);
|
|
138
|
+
while (!stopped) {
|
|
139
|
+
if (!ownsRegistration())
|
|
140
|
+
await stop(false);
|
|
141
|
+
try {
|
|
142
|
+
if (readFileSync(pidFile, "utf8").trim() !== String(process.pid))
|
|
143
|
+
await stop(false);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
await stop(false);
|
|
147
|
+
}
|
|
148
|
+
if (sessionOverMarked(projectDir))
|
|
149
|
+
await stop(true);
|
|
150
|
+
if (existsSync(path.join(stateDir, "shutdown")))
|
|
151
|
+
await stop(true);
|
|
152
|
+
try {
|
|
153
|
+
// Anchor expiry at request START: delayed responses cannot lengthen the lease.
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
const lease = await captureLease(config.backendUrl, config.token);
|
|
156
|
+
if (!lease.active)
|
|
157
|
+
return await stop(true);
|
|
158
|
+
const registration = JSON.parse(readFileSync(HOSTED_CAPTURE_FILE, "utf8"));
|
|
159
|
+
if (registration.projectDir !== projectDir)
|
|
160
|
+
await stop(false);
|
|
161
|
+
atomicPrivateJson(HOSTED_CAPTURE_FILE, { ...registration, expiresAt: started + lease.leaseSeconds * 1000, receipt: lease.receipt, serverTime: lease.serverTime, leaseRequestStarted: started, leaseResponseReceivedAt: lease.receivedAt });
|
|
162
|
+
writeFileSync(nonce, String(Date.now()), { mode: 0o600 });
|
|
163
|
+
writeFileSync(ready, String(process.pid), { mode: 0o600 });
|
|
164
|
+
if (Date.now() - lastFlushAttempt >= 5000) {
|
|
165
|
+
lastFlushAttempt = Date.now();
|
|
166
|
+
await flush();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
process.stderr.write(`[hosted capture] authorization/upload unavailable; capture lease will expire\n`);
|
|
171
|
+
}
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
run().catch(() => stop(false));
|
|
176
|
+
//# sourceMappingURL=hosted-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-watcher.js","sourceRoot":"","sources":["../../src/lib/hosted-watcher.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AAC9G,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AACnH,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAG7G,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAClC,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACnC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;AACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAiB,CAAA;AACnG,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAA;AAClD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;AAClD,IAAI,OAAO,GAAG,KAAK,CAAA;AACnB,IAAI,gBAAgB,GAAG,CAAC,CAAA;AACxB,IAAI,MAAM,GAAG,CAAC,CAAA;AACd,IAAI,CAAC;IAAC,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAA;AAAC,CAAC;AAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,CAAC;AACnH,SAAS,gBAAgB;IACvB,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAA;IAAC,CAAC;AACvH,CAAC;AACD,KAAK,UAAU,IAAI,CAAC,MAAe;IACjC,IAAI,OAAO;QAAE,OAAO,MAAM,IAAI,OAAO,CAAQ,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACtD,OAAO,GAAG,IAAI,CAAA;IACd,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,CAAC;QAAC,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IACrG,IAAI,gBAAgB,EAAE,IAAI,OAAO,EAAE,CAAC;QAClC,0EAA0E;QAC1E,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAA;QAC1E,iBAAiB,CAAC,mBAAmB,EAAE,EAAE,GAAG,YAAY,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;QACzE,IAAI,MAAM,EAAE,CAAC;YACX,uEAAuE;YACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;YACnC,IAAI,CAAC;gBAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC,CAAC,wBAAwB,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,sCAAsC,CAAC,CAAC;YACpI,sBAAsB,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,sBAAsB,CAAC,CAAA;YACxE,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;YAC1C,IAAI,OAAO,GAAG,KAAK,CAAA;YACnB,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,EAAE,CAAC;gBAChD,IAAI,YAA+C,CAAA;gBACnD,IAAI,CAAC;oBACH,oEAAoE;oBACpE,wEAAwE;oBACxE,YAAY,GAAG,MAAM,uBAAuB,CAAC,GAAG,mBAAmB,OAAO,CAAC,CAAA;oBAC3E,8BAA8B,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;oBACxD,IAAI,gBAAgB,EAAE;wBAAE,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;oBACpE,OAAO,GAAG,IAAI,CAAA;gBAChB,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;gBACxD,CAAC;wBAAS,CAAC;oBACT,IAAI,YAAY;wBAAE,MAAM,YAAY,EAAE,CAAA;gBACxC,CAAC;YACH,CAAC;YACD,yEAAyE;YACzE,wEAAwE;YACxE,yEAAyE;YACzE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3D,IAAI,CAAC,OAAO;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gHAAgH,CAAC,CAAA;QACtJ,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QAAC,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,MAAM,CAAC,KAAK,EAAE,EAAC,KAAK,EAAC,IAAI,EAAC,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAC1H,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AACD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;AACjD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;AAEhD,KAAK,UAAU,KAAK;IAClB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;IACrC,IAAI,KAAa,CAAA;IACjB,IAAI,CAAC;QACH,IAAI,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI;YAAE,MAAM,GAAG,CAAC,CAAA;QAC3C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAA;QAC5C,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5E,CAAC;YAAS,CAAC;QAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAAC,CAAC;IAC3B,yEAAyE;IACzE,IAAI,GAAG,GAAG,CAAC,CAAC,CAAA;IACZ,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;YAAC,GAAG,GAAG,CAAC,CAAC;YAAC,IAAI,EAAE,KAAK,KAAK,GAAG;gBAAE,MAAK;QAAC,CAAC;IAC9D,CAAC;IACD,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACzB,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACjF,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,eAAe,EAAE;QAC9D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,WAAW,EAAE,yBAAyB,EAAE,IAAI,EAAE;QACjM,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAC,MAAM,EAAC,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;KAClE,CAAC,CAAA;IACF,kFAAkF;IAClF,MAAM,eAAe,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,EAA8C,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1G,MAAM,YAAY,GAAG,eAAe,EAAE,OAAO,KAAK,IAAI,CAAA;IACtD,IAAI,YAAY,IAAI,OAAO,eAAe,EAAE,SAAS,KAAK,QAAQ,IAAI,eAAe,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QACpG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,eAAe,CAAC,SAAS,qGAAqG,CAAC,CAAA;IAC1K,CAAC;IACD,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAA;QACjB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,EAAC,IAAI,EAAC,KAAK,EAAC,CAAC,CAAA;QACjF,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AACD,KAAK,UAAU,GAAG;IAChB,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;IACtD,IAAI,CAAC,gBAAgB,EAAE,IAAI,iBAAiB,CAAC,UAAU,CAAC;QAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1E,OAAO,CAAC,OAAO,EAAE,CAAC;QAChB,IAAI,CAAC,gBAAgB,EAAE;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1C,IAAI,CAAC;YACH,IAAI,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;QACrF,CAAC;QAAC,MAAM,CAAC;YAAC,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;QAAC,CAAC;QAC7B,IAAI,iBAAiB,CAAC,UAAU,CAAC;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;QACnD,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;QACjE,IAAI,CAAC;YACH,+EAA+E;YAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC1B,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,UAAW,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;YAClE,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;YAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAA;YAC1E,IAAI,YAAY,CAAC,UAAU,KAAK,UAAU;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;YAC7D,iBAAiB,CAAC,mBAAmB,EAAE,EAAE,GAAG,YAAY,EAAE,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,mBAAmB,EAAE,OAAO,EAAE,uBAAuB,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;YAC1O,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,EAAC,IAAI,EAAC,KAAK,EAAC,CAAC,CAAA;YACtD,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAC,IAAI,EAAC,KAAK,EAAC,CAAC,CAAA;YACvD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,IAAI,IAAI,EAAE,CAAC;gBAC1C,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;gBAC7B,MAAM,KAAK,EAAE,CAAA;YACf,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gFAAgF,CAAC,CAAA;QACxG,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAA;IACzD,CAAC;AACH,CAAC;AACD,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** LocalCommand goes through both OpenSSH token expansion and the local shell. */
|
|
2
|
+
export declare function hookCommand(node: string, helper: string, alias: string, platform?: NodeJS.Platform): string;
|
|
3
|
+
/** Copy builtin-only modules out of npx's disposable cache. Content-addressed
|
|
4
|
+
* directories let existing SSH connections finish on their installed version. */
|
|
5
|
+
export declare function installSshForwardHook(alias: string, home?: string, source?: string): string;
|
|
6
|
+
//# sourceMappingURL=install-ssh-forward-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-ssh-forward-hook.d.ts","sourceRoot":"","sources":["../../src/lib/install-ssh-forward-hook.ts"],"names":[],"mappings":"AAMA,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,kBAAmB,GAAG,MAAM,CAc5G;AAED;iFACiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAe,EAAE,MAAM,SAA+C,GAAG,MAAM,CAsBvI"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, renameSync, rmSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
/** LocalCommand goes through both OpenSSH token expansion and the local shell. */
|
|
7
|
+
export function hookCommand(node, helper, alias, platform = process.platform) {
|
|
8
|
+
if (!/^litmus-[a-zA-Z0-9-]+$/.test(alias))
|
|
9
|
+
throw new Error("Invalid SSH alias");
|
|
10
|
+
if (/[\r\n\0]/.test(node + helper))
|
|
11
|
+
throw new Error("Invalid helper path");
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
// Native OpenSSH runs cmd; Git's SSH runs sh. Use their common syntax and
|
|
14
|
+
// encode the path so neither shell can expand a username's metacharacters.
|
|
15
|
+
// Node is already required on PATH for the candidate's connection setup.
|
|
16
|
+
const encoded = Buffer.from(helper).toString("base64");
|
|
17
|
+
return `node -e "import(require('url').pathToFileURL(Buffer.from('${encoded}','base64').toString()).href).then(m=>m.launchHook(['launch','${alias}'])).catch(()=>console.error('Automatic forwarding unavailable; rerun Litmus setup.'))"`;
|
|
18
|
+
}
|
|
19
|
+
const quote = (value) => {
|
|
20
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
21
|
+
};
|
|
22
|
+
return [node, helper, "launch", alias].map(quote).join(" ").replace(/%/g, "%%");
|
|
23
|
+
}
|
|
24
|
+
/** Copy builtin-only modules out of npx's disposable cache. Content-addressed
|
|
25
|
+
* directories let existing SSH connections finish on their installed version. */
|
|
26
|
+
export function installSshForwardHook(alias, home = os.homedir(), source = path.dirname(fileURLToPath(import.meta.url))) {
|
|
27
|
+
const names = ["ssh-forward-hook.js", "auto-forward.js", "port-forward.js", "ssh-client.js"];
|
|
28
|
+
const files = names.map((name) => [name, readFileSync(path.join(source, name))]);
|
|
29
|
+
const hash = createHash("sha256");
|
|
30
|
+
for (const [name, contents] of files)
|
|
31
|
+
hash.update(name).update(contents);
|
|
32
|
+
const destination = path.join(home, ".ssh", "litmus-forward", hash.digest("hex").slice(0, 20));
|
|
33
|
+
mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
34
|
+
const staging = mkdtempSync(path.join(path.dirname(destination), ".install-"));
|
|
35
|
+
try {
|
|
36
|
+
for (const [name, contents] of [...files, ["package.json", Buffer.from('{"type":"module"}')]]) {
|
|
37
|
+
writeFileSync(path.join(staging, name), contents, { flag: "wx", mode: 0o600 });
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
renameSync(staging, destination);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!existsSync(destination))
|
|
44
|
+
throw error;
|
|
45
|
+
// Another setup already published this complete immutable version.
|
|
46
|
+
for (const [name, contents] of files) {
|
|
47
|
+
if (!readFileSync(path.join(destination, name)).equals(contents))
|
|
48
|
+
throw new Error("Forwarding helper installation is inconsistent");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
rmSync(staging, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
return hookCommand(process.execPath, path.join(destination, "ssh-forward-hook.js"), alias);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=install-ssh-forward-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-ssh-forward-hook.js","sourceRoot":"","sources":["../../src/lib/install-ssh-forward-hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PortForward {
|
|
2
|
+
local: number;
|
|
3
|
+
remote: number;
|
|
4
|
+
}
|
|
5
|
+
/** `ssh` arguments for the tunnel. */
|
|
6
|
+
export declare function tunnelArgs(alias: string, forwards: PortForward[]): string[];
|
|
7
|
+
/** Whether this computer can listen on `port`, which ssh is about to need. */
|
|
8
|
+
export declare function localPortFree(port: number): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Whether something accepts connections on `port` here. ssh binds a forward's local
|
|
11
|
+
* port only once it has connected and authenticated, so this is how we know the tunnel
|
|
12
|
+
* is up. A connect, never a listen: a listen could hold the port at the moment ssh
|
|
13
|
+
* binds it, and `ExitOnForwardFailure` would end the tunnel over it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function localPortListening(port: number): Promise<boolean>;
|
|
16
|
+
//# sourceMappingURL=port-forward.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-forward.d.ts","sourceRoot":"","sources":["../../src/lib/port-forward.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAE9D,sCAAsC;AACtC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,EAAE,CAkB3E;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASjE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
/** `ssh` arguments for the tunnel. */
|
|
3
|
+
export function tunnelArgs(alias, forwards) {
|
|
4
|
+
return [
|
|
5
|
+
"-N", "-o", "PermitLocalCommand=no", "-T",
|
|
6
|
+
"-o", "BatchMode=yes",
|
|
7
|
+
"-o", "ConnectTimeout=15",
|
|
8
|
+
// Each child owns its connection, even with a user-wide ControlMaster setting.
|
|
9
|
+
"-o", "ControlMaster=no",
|
|
10
|
+
"-o", "ControlPath=none",
|
|
11
|
+
// A forward that cannot bind is a failure to report, not a tunnel that is half up.
|
|
12
|
+
"-o", "ExitOnForwardFailure=yes",
|
|
13
|
+
"-o", "ServerAliveInterval=30",
|
|
14
|
+
"-o", "ServerAliveCountMax=3",
|
|
15
|
+
// Every connection to a port with nothing running on it would otherwise print
|
|
16
|
+
// "channel N: open failed" into the candidate's terminal.
|
|
17
|
+
"-o", "LogLevel=QUIET",
|
|
18
|
+
...forwards.flatMap((f) => ["-L", `127.0.0.1:${f.local}:localhost:${f.remote}`]),
|
|
19
|
+
alias,
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/** Whether this computer can listen on `port`, which ssh is about to need. */
|
|
23
|
+
export function localPortFree(port) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
server.once("error", () => resolve(false));
|
|
27
|
+
server.listen(port, "127.0.0.1", () => server.close(() => resolve(true)));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether something accepts connections on `port` here. ssh binds a forward's local
|
|
32
|
+
* port only once it has connected and authenticated, so this is how we know the tunnel
|
|
33
|
+
* is up. A connect, never a listen: a listen could hold the port at the moment ssh
|
|
34
|
+
* binds it, and `ExitOnForwardFailure` would end the tunnel over it.
|
|
35
|
+
*/
|
|
36
|
+
export function localPortListening(port) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const socket = net.connect({ port, host: "127.0.0.1" });
|
|
39
|
+
socket.setTimeout(1000);
|
|
40
|
+
const done = (up) => { socket.destroy(); resolve(up); };
|
|
41
|
+
socket.once("connect", () => done(true));
|
|
42
|
+
socket.once("error", () => done(false));
|
|
43
|
+
socket.once("timeout", () => done(false));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=port-forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-forward.js","sourceRoot":"","sources":["../../src/lib/port-forward.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,KAAK,CAAA;AAIrB,sCAAsC;AACtC,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,QAAuB;IAC/D,OAAO;QACL,IAAI,EAAE,IAAI,EAAE,uBAAuB,EAAE,IAAI;QACzC,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,mBAAmB;QACzB,+EAA+E;QAC/E,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,kBAAkB;QACxB,mFAAmF;QACnF,IAAI,EAAE,0BAA0B;QAChC,IAAI,EAAE,wBAAwB;QAC9B,IAAI,EAAE,uBAAuB;QAC7B,8EAA8E;QAC9E,0DAA0D;QAC1D,IAAI,EAAE,gBAAgB;QACtB,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAChF,KAAK;KACN,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAA;QACjC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;QAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;QACvD,MAAM,CAAC,UAAU,CAAC,IAAK,CAAC,CAAA;QACxB,MAAM,IAAI,GAAG,CAAC,EAAW,EAAE,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA,CAAC,CAAC,CAAA;QAC/D,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACxC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACvC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { fetchSubmissionSaved, getConnection } from "./api.js";
|
|
2
|
+
/**
|
|
3
|
+
* ENG-3265: what the server says about this assessment, asked when a workspace
|
|
4
|
+
* connection is lost and before the loss is called a failure.
|
|
5
|
+
*
|
|
6
|
+
* Submitting, running out the clock and a company lock all end SSH access on purpose:
|
|
7
|
+
* the bastion closes the authenticated connection and refuses the next one. To ssh that
|
|
8
|
+
* looks exactly like a network fault (exit 255, every retry within seconds), so a
|
|
9
|
+
* `litmus forward` that only counted exits reported an expected closure as
|
|
10
|
+
* "Couldn't keep the connection to your workspace open", alerting platform health
|
|
11
|
+
* and telling a candidate who had just submitted to start it again.
|
|
12
|
+
*
|
|
13
|
+
* The answer comes from the same gate the bastion's own refusal comes from:
|
|
14
|
+
* `/workspace/connection` is behind `require_active_candidate`, whose
|
|
15
|
+
* `candidate_access_denial` refuses a terminal candidate (`assessment_closed`) and a
|
|
16
|
+
* locked one (`assessment_locked`) on the predicates `/resolve-ssh` uses. Only then is
|
|
17
|
+
* `/workspace/submit-status` asked which of the two closings it was, and "submitted" is
|
|
18
|
+
* said only when it confirms a final submission exists. Anything that is not one of those
|
|
19
|
+
* answers stays `unknown`: a lookup that failed proves nothing, so it never ends the
|
|
20
|
+
* forward and never claims the assessment was submitted.
|
|
21
|
+
*/
|
|
22
|
+
export type SessionState = {
|
|
23
|
+
kind: "open";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "submitted";
|
|
26
|
+
} | {
|
|
27
|
+
kind: "over";
|
|
28
|
+
} | {
|
|
29
|
+
kind: "locked";
|
|
30
|
+
} | {
|
|
31
|
+
kind: "unknown";
|
|
32
|
+
reason: "network" | "rejected" | "server" | "timeout" | "unexpected" | "cancelled";
|
|
33
|
+
};
|
|
34
|
+
export type EndedSessionState = Extract<SessionState, {
|
|
35
|
+
kind: "submitted" | "over" | "locked";
|
|
36
|
+
}>;
|
|
37
|
+
export declare function sessionEnded(state: SessionState): state is EndedSessionState;
|
|
38
|
+
export interface SessionStateDeps {
|
|
39
|
+
getConnection: typeof getConnection;
|
|
40
|
+
fetchSubmissionSaved: typeof fetchSubmissionSaved;
|
|
41
|
+
deadlineMs: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function readSessionState(backendUrl: string, token: string, deps?: SessionStateDeps, signal?: AbortSignal): Promise<SessionState>;
|
|
44
|
+
/**
|
|
45
|
+
* Which closing an `assessment_closed` was. Over is always true of a closed assessment;
|
|
46
|
+
* submitted only when the server confirms a final submission.
|
|
47
|
+
*/
|
|
48
|
+
export declare function closedState(backendUrl: string, token: string, deps?: Pick<SessionStateDeps, "fetchSubmissionSaved">, signal?: AbortSignal): Promise<{
|
|
49
|
+
kind: "submitted";
|
|
50
|
+
} | {
|
|
51
|
+
kind: "over";
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* What the candidate reads when the forward stops because the assessment closed: what
|
|
55
|
+
* happened, then what to do. The two sentences about a closed workspace are the ones the
|
|
56
|
+
* bastion itself sends (`_SUBMITTED_DETAIL` and `_ASSESSMENT_OVER_DETAIL` in
|
|
57
|
+
* backend/api/ingress.py), and the lock copy is `litmus pause`'s, so every surface says
|
|
58
|
+
* the same thing. No restart hint: running the command again cannot reopen the workspace.
|
|
59
|
+
*/
|
|
60
|
+
export declare function sessionEndedLines(state: EndedSessionState, pageUrl: string): string[];
|
|
61
|
+
//# sourceMappingURL=session-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-state.d.ts","sourceRoot":"","sources":["../../src/lib/session-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAA4C,MAAM,UAAU,CAAA;AAExG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,CAAA;CAAE,CAAA;AAE3G,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAC,CAAA;AAEhG,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,KAAK,IAAI,iBAAiB,CAE5E;AAOD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,OAAO,aAAa,CAAA;IACnC,oBAAoB,EAAE,OAAO,oBAAoB,CAAA;IACjD,UAAU,EAAE,MAAM,CAAA;CACnB;AAID,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,gBAA8B,EACpC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,CAAC,CAiBvB;AAeD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,IAAI,CAAC,gBAAgB,EAAE,sBAAsB,CAAe,EAClE,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnD;AASD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBrF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fetchSubmissionSaved, getConnection, isAuthError, isNetworkError, ServerError } from "./api.js";
|
|
2
|
+
export function sessionEnded(state) {
|
|
3
|
+
return state.kind === "submitted" || state.kind === "over" || state.kind === "locked";
|
|
4
|
+
}
|
|
5
|
+
/** Per request. A dropped tunnel waits on this, so a dead network must not hold it long. */
|
|
6
|
+
const LOOKUP_TIMEOUT_MS = 5000;
|
|
7
|
+
/** The whole lookup, retries included. Past it the answer is `unknown`. */
|
|
8
|
+
const LOOKUP_DEADLINE_MS = 10000;
|
|
9
|
+
const defaultDeps = { getConnection, fetchSubmissionSaved, deadlineMs: LOOKUP_DEADLINE_MS };
|
|
10
|
+
export async function readSessionState(backendUrl, token, deps = defaultDeps, signal) {
|
|
11
|
+
// Ctrl+C during a lookup is `cancelled` at once and aborts the requests in flight, so
|
|
12
|
+
// an unreachable API never holds the command open (`signal` is the forward's stop).
|
|
13
|
+
if (signal?.aborted)
|
|
14
|
+
return { kind: "unknown", reason: "cancelled" };
|
|
15
|
+
let timer;
|
|
16
|
+
let cancel;
|
|
17
|
+
const deadline = new Promise((resolve) => {
|
|
18
|
+
timer = setTimeout(() => resolve({ kind: "unknown", reason: "timeout" }), deps.deadlineMs);
|
|
19
|
+
cancel = () => resolve({ kind: "unknown", reason: "cancelled" });
|
|
20
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([lookup(backendUrl, token, deps, signal), deadline]);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
if (cancel)
|
|
28
|
+
signal?.removeEventListener("abort", cancel);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function lookup(backendUrl, token, deps, signal) {
|
|
32
|
+
try {
|
|
33
|
+
await deps.getConnection(backendUrl, token, LOOKUP_TIMEOUT_MS, signal);
|
|
34
|
+
return { kind: "open" };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err instanceof ServerError && err.statusCode === 403 && err.errorCode === "assessment_locked")
|
|
38
|
+
return { kind: "locked" };
|
|
39
|
+
if (err instanceof ServerError && err.statusCode === 403 && err.errorCode === "assessment_closed") {
|
|
40
|
+
return closedState(backendUrl, token, deps, signal);
|
|
41
|
+
}
|
|
42
|
+
return { kind: "unknown", reason: unknownReason(err) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Which closing an `assessment_closed` was. Over is always true of a closed assessment;
|
|
47
|
+
* submitted only when the server confirms a final submission.
|
|
48
|
+
*/
|
|
49
|
+
export async function closedState(backendUrl, token, deps = defaultDeps, signal) {
|
|
50
|
+
try {
|
|
51
|
+
return (await deps.fetchSubmissionSaved(backendUrl, token, LOOKUP_TIMEOUT_MS, signal)) ? { kind: "submitted" } : { kind: "over" };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { kind: "over" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function unknownReason(err) {
|
|
58
|
+
if (isNetworkError(err))
|
|
59
|
+
return "network";
|
|
60
|
+
if (isAuthError(err))
|
|
61
|
+
return "rejected";
|
|
62
|
+
if (err instanceof ServerError && (err.statusCode ?? 0) >= 500)
|
|
63
|
+
return "server";
|
|
64
|
+
return "unexpected";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What the candidate reads when the forward stops because the assessment closed: what
|
|
68
|
+
* happened, then what to do. The two sentences about a closed workspace are the ones the
|
|
69
|
+
* bastion itself sends (`_SUBMITTED_DETAIL` and `_ASSESSMENT_OVER_DETAIL` in
|
|
70
|
+
* backend/api/ingress.py), and the lock copy is `litmus pause`'s, so every surface says
|
|
71
|
+
* the same thing. No restart hint: running the command again cannot reopen the workspace.
|
|
72
|
+
*/
|
|
73
|
+
export function sessionEndedLines(state, pageUrl) {
|
|
74
|
+
switch (state.kind) {
|
|
75
|
+
case "submitted":
|
|
76
|
+
return [
|
|
77
|
+
"Your assessment has been submitted, so this workspace is now closed. The forward has stopped.",
|
|
78
|
+
`Your work was captured before it closed. Open the assessment page to see its status: ${pageUrl}`,
|
|
79
|
+
];
|
|
80
|
+
case "over":
|
|
81
|
+
return [
|
|
82
|
+
"This assessment is over, so its workspace is now closed. The forward has stopped.",
|
|
83
|
+
`Open the assessment page to see its status: ${pageUrl}`,
|
|
84
|
+
];
|
|
85
|
+
case "locked":
|
|
86
|
+
return [
|
|
87
|
+
"The hiring team has locked this assessment, so the forward has stopped.",
|
|
88
|
+
"Your work is saved. Run the same litmus forward command again when they reopen it.",
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=session-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-state.js","sourceRoot":"","sources":["../../src/lib/session-state.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/ssh-client.d.ts
CHANGED
|
@@ -75,14 +75,23 @@ export declare const SSH_CONNECTION_FAILED = 255;
|
|
|
75
75
|
* not be started. Our own Ctrl-C is ignored while it runs, because the terminal sends it
|
|
76
76
|
* to both processes and the candidate meant it for the shell, not for us.
|
|
77
77
|
*/
|
|
78
|
-
export declare function openSshSession(alias: string, executable?: string, spawnFn?: typeof spawn): Promise<number | null>;
|
|
78
|
+
export declare function openSshSession(alias: string, executable?: string, spawnFn?: typeof spawn, signal?: AbortSignal): Promise<number | null>;
|
|
79
79
|
/**
|
|
80
80
|
* ENG-3188: `ssh -N` holding `args`' forwards open in this terminal: its exit status,
|
|
81
81
|
* or null when it could not be started. Ctrl-C is ignored here for the same reason as
|
|
82
82
|
* `openSshSession`, and `onInterrupt` tells the caller it was pressed, so a stop the
|
|
83
83
|
* candidate asked for is not mistaken for a dropped connection.
|
|
84
|
+
*
|
|
85
|
+
* ENG-3265: with `onStderr`, ssh's stderr is passed through to ours unchanged and also
|
|
86
|
+
* handed to the caller, which classifies it into fixed labels for a failure's
|
|
87
|
+
* diagnostics. Prompts are untouched: ssh asks them on the terminal, not on stderr.
|
|
84
88
|
*/
|
|
85
|
-
export declare function openSshTunnel(args: string[], onInterrupt: () => void, executable?: string, spawnFn?: typeof spawn): Promise<number | null>;
|
|
89
|
+
export declare function openSshTunnel(args: string[], onInterrupt: () => void, executable?: string, spawnFn?: typeof spawn, onStderr?: (text: string) => void): Promise<number | null>;
|
|
90
|
+
/** ENG-3201: one owned child, no per-child process signal handlers. */
|
|
91
|
+
export declare function startManagedSshTunnel(args: string[], spawnFn?: typeof spawn, detached?: boolean): {
|
|
92
|
+
exited: Promise<number | null>;
|
|
93
|
+
stop: () => Promise<void>;
|
|
94
|
+
};
|
|
86
95
|
/**
|
|
87
96
|
* The editor's Windows user `settings.json`. Built with `path.win32` explicitly, not the
|
|
88
97
|
* host's `path`: this names a Windows file whatever runs it (gotcha 018). Null without
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssh-client.d.ts","sourceRoot":"","sources":["../../src/lib/ssh-client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAGhD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,YAAY,EAAE,OAAO,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAA;CACd;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAIzE;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAGjE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,SAAQ,GAAG,MAAM,GAAG,IAAI,CAKtE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,SAAS,CAAA;IACvB,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC;AAED,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAU9C,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,UAAU,SAAQ,EAClB,IAAI,GAAE,gBAAuC,GAC5C,OAAO,CAAC,YAAY,CAAC,CAUvB;AAyCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,UAAQ,GAAG,MAAM,CAaxG;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAA;AAElD,kGAAkG;AAClG,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,SAAQ,EAAE,OAAO,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"ssh-client.d.ts","sourceRoot":"","sources":["../../src/lib/ssh-client.ts"],"names":[],"mappings":"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"}
|