instar 0.6.4 → 0.6.5
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.
|
@@ -150,13 +150,22 @@ export class SessionManager extends EventEmitter {
|
|
|
150
150
|
return false;
|
|
151
151
|
// Verify Claude process is running inside the tmux session
|
|
152
152
|
try {
|
|
153
|
-
const
|
|
153
|
+
const paneInfo = execFileSync(this.config.tmuxPath, ['display-message', '-t', `=${tmuxSession}:`, '-p', '#{pane_current_command}||#{pane_start_command}'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
154
|
+
const [paneCmd, startCmd] = paneInfo.split('||');
|
|
154
155
|
// Claude Code runs as 'claude' or 'node' process
|
|
155
156
|
if (paneCmd && (paneCmd.includes('claude') || paneCmd.includes('node'))) {
|
|
156
157
|
return true;
|
|
157
158
|
}
|
|
158
|
-
// If pane command is bash/zsh/sh,
|
|
159
|
+
// If pane command is bash/zsh/sh, check whether the session was launched
|
|
160
|
+
// with a direct command (e.g., a bash script as claudePath). In that case
|
|
161
|
+
// bash IS the expected running process — not a leftover shell after Claude exits.
|
|
162
|
+
// tmux kills sessions launched with direct commands when the command exits,
|
|
163
|
+
// so if has-session succeeds and start_command is non-empty, it's still running.
|
|
159
164
|
if (paneCmd === 'bash' || paneCmd === 'zsh' || paneCmd === 'sh') {
|
|
165
|
+
if (startCmd && startCmd !== paneCmd) {
|
|
166
|
+
// Session was launched with a specific command (not a bare shell) — still alive
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
160
169
|
return false;
|
|
161
170
|
}
|
|
162
171
|
// For any other command, assume alive (could be a Claude subprocess)
|
|
@@ -18,5 +18,10 @@ export declare function rateLimiter(windowMs?: number, maxRequests?: number): (r
|
|
|
18
18
|
* Returns 408 if the request takes longer than the timeout.
|
|
19
19
|
*/
|
|
20
20
|
export declare function requestTimeout(timeoutMs?: number): (req: Request, res: Response, next: NextFunction) => void;
|
|
21
|
+
/**
|
|
22
|
+
* HMAC-sign a view path so the URL can be opened in a browser without exposing the auth token.
|
|
23
|
+
* The signature is path-specific — sharing a signed URL only grants access to that one view.
|
|
24
|
+
*/
|
|
25
|
+
export declare function signViewPath(viewPath: string, authToken: string): string;
|
|
21
26
|
export declare function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction): void;
|
|
22
27
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Express middleware — JSON parsing, CORS, auth, error handling.
|
|
3
3
|
*/
|
|
4
|
-
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
5
5
|
export function corsMiddleware(req, res, next) {
|
|
6
6
|
// Restrict CORS to localhost origins only — this is a local management API
|
|
7
7
|
const origin = req.headers.origin;
|
|
@@ -32,21 +32,20 @@ export function authMiddleware(authToken) {
|
|
|
32
32
|
next();
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
else if (queryToken) {
|
|
44
|
-
token = queryToken;
|
|
35
|
+
// View routes support signed URLs for browser access (see ?sig= below)
|
|
36
|
+
if (req.path.startsWith('/view/') && req.method === 'GET') {
|
|
37
|
+
const sig = typeof req.query.sig === 'string' ? req.query.sig : null;
|
|
38
|
+
if (sig && verifyViewSignature(req.path, sig, authToken)) {
|
|
39
|
+
next();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
45
42
|
}
|
|
46
|
-
|
|
43
|
+
const header = req.headers.authorization;
|
|
44
|
+
if (!header || !header.startsWith('Bearer ')) {
|
|
47
45
|
res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
48
46
|
return;
|
|
49
47
|
}
|
|
48
|
+
const token = header.slice(7);
|
|
50
49
|
// Hash both sides so lengths are always equal — prevents timing leak of token length
|
|
51
50
|
const ha = createHash('sha256').update(token).digest();
|
|
52
51
|
const hb = createHash('sha256').update(authToken).digest();
|
|
@@ -117,6 +116,23 @@ export function requestTimeout(timeoutMs = 30_000) {
|
|
|
117
116
|
next();
|
|
118
117
|
};
|
|
119
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* HMAC-sign a view path so the URL can be opened in a browser without exposing the auth token.
|
|
121
|
+
* The signature is path-specific — sharing a signed URL only grants access to that one view.
|
|
122
|
+
*/
|
|
123
|
+
export function signViewPath(viewPath, authToken) {
|
|
124
|
+
return createHmac('sha256', authToken).update(viewPath).digest('hex');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Verify a signed view URL. Returns true if the sig matches the path.
|
|
128
|
+
*/
|
|
129
|
+
function verifyViewSignature(viewPath, sig, authToken) {
|
|
130
|
+
const expected = createHmac('sha256', authToken).update(viewPath).digest();
|
|
131
|
+
const provided = Buffer.from(sig, 'hex');
|
|
132
|
+
if (expected.length !== provided.length)
|
|
133
|
+
return false;
|
|
134
|
+
return timingSafeEqual(expected, provided);
|
|
135
|
+
}
|
|
120
136
|
export function errorHandler(err, _req, res, _next) {
|
|
121
137
|
const message = err instanceof Error ? err.message : String(err);
|
|
122
138
|
console.error(`[server] Error: ${message}`);
|
package/dist/server/routes.js
CHANGED
|
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
9
9
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { rateLimiter } from './middleware.js';
|
|
12
|
+
import { rateLimiter, signViewPath } from './middleware.js';
|
|
13
13
|
// Validation patterns for route parameters
|
|
14
14
|
const SESSION_NAME_RE = /^[a-zA-Z0-9_-]{1,200}$/;
|
|
15
15
|
const JOB_SLUG_RE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
@@ -880,13 +880,15 @@ export function createRoutes(ctx) {
|
|
|
880
880
|
});
|
|
881
881
|
// ── Private Views (auth-gated rendered markdown) ────────────────
|
|
882
882
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
883
|
-
/** Build a browser-clickable tunnel URL with
|
|
883
|
+
/** Build a browser-clickable tunnel URL with HMAC signature for auth */
|
|
884
884
|
function viewTunnelUrl(viewId) {
|
|
885
885
|
const base = ctx.tunnel?.getExternalUrl(`/view/${viewId}`);
|
|
886
886
|
if (!base)
|
|
887
887
|
return null;
|
|
888
888
|
if (ctx.config.authToken) {
|
|
889
|
-
|
|
889
|
+
const viewPath = `/view/${viewId}`;
|
|
890
|
+
const sig = signViewPath(viewPath, ctx.config.authToken);
|
|
891
|
+
return `${base}?sig=${sig}`;
|
|
890
892
|
}
|
|
891
893
|
return base;
|
|
892
894
|
}
|