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 paneCmd = execFileSync(this.config.tmuxPath, ['display-message', '-t', `=${tmuxSession}:`, '-p', '#{pane_current_command}'], { encoding: 'utf-8', timeout: 5000 }).trim();
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, Claude may have exited — session is dead
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
- // Accept token from Authorization header or ?token= query parameter.
36
- // Query parameter enables browser-friendly access to rendered views.
37
- const header = req.headers.authorization;
38
- const queryToken = typeof req.query.token === 'string' ? req.query.token : null;
39
- let token;
40
- if (header?.startsWith('Bearer ')) {
41
- token = header.slice(7);
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
- else {
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}`);
@@ -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 token embedded as query param */
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
- return `${base}?token=${encodeURIComponent(ctx.config.authToken)}`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",