@m8i-51/shoal 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m8i-51/shoal",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Multi-agent web exploration framework — finds bugs, UX issues, and missing features by running AI agents against your app",
6
6
  "repository": {
@@ -38,6 +38,7 @@
38
38
  "@clack/prompts": "^1.3.0",
39
39
  "dotenv": "^17.3.1",
40
40
  "express": "^5.2.1",
41
+ "express-rate-limit": "^8.5.0",
41
42
  "openai": "^6.33.0",
42
43
  "playwright": "^1.59.1",
43
44
  "tsx": "^4.21.0"
package/server/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import "dotenv/config";
2
2
  import express from "express";
3
+ import { rateLimit } from "express-rate-limit";
3
4
  import { fileURLToPath } from "url";
4
- import { dirname, join } from "path";
5
+ import { dirname, join, resolve } from "path";
5
6
  import { existsSync, readFileSync, writeFileSync } from "fs";
6
7
  import { listRuns, getReportPath } from "./runs.js";
7
8
  import { activeSessions, spawnRun, cancelSession } from "./runner.js";
@@ -15,11 +16,23 @@ function specFilePath(baseUrl: string): string {
15
16
  }
16
17
  }
17
18
 
19
+ const RUN_ID_RE = /^run_\d+$/;
20
+ function isValidRunId(id: string): boolean {
21
+ return RUN_ID_RE.test(id);
22
+ }
23
+
24
+ const logsBase = resolve(process.cwd(), "logs");
25
+ function safeLogPath(filename: string): string | null {
26
+ const p = resolve(logsBase, filename);
27
+ return p.startsWith(logsBase + "/") ? p : null;
28
+ }
29
+
18
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
31
  const app = express();
20
32
  const PORT = parseInt(process.env.PORT ?? "4000", 10);
21
33
 
22
34
  app.use(express.json());
35
+ app.use(rateLimit({ windowMs: 60_000, limit: 120 }));
23
36
 
24
37
  // ----------------------------------------------------------------
25
38
  // API: product spec (goals)
@@ -80,7 +93,12 @@ app.get("/api/runs", (_req, res) => {
80
93
  // API: serve HTML report for a run
81
94
  // ----------------------------------------------------------------
82
95
  app.get("/api/runs/:runId/report", (req, res) => {
83
- const reportPath = getReportPath(req.params.runId);
96
+ const { runId } = req.params;
97
+ if (!isValidRunId(runId)) {
98
+ res.status(400).json({ error: "invalid run id" });
99
+ return;
100
+ }
101
+ const reportPath = getReportPath(runId);
84
102
  if (!reportPath) {
85
103
  res.status(404).json({ error: "report not found" });
86
104
  return;
@@ -157,14 +175,18 @@ function sseStream(req: express.Request, res: express.Response, sessionId: strin
157
175
  // API: SSE — /api/sessions/:sessionId/events(後方互換)
158
176
  // ----------------------------------------------------------------
159
177
  app.get("/api/sessions/:sessionId/events", (req, res) => {
160
- sseStream(req, res, req.params.sessionId);
178
+ const { sessionId } = req.params;
179
+ if (!isValidRunId(sessionId)) { res.status(400).json({ error: "invalid session id" }); return; }
180
+ sseStream(req, res, sessionId);
161
181
  });
162
182
 
163
183
  // ----------------------------------------------------------------
164
184
  // API: SSE — /api/runs/:runId/events(詳細ページ用)
165
185
  // ----------------------------------------------------------------
166
186
  app.get("/api/runs/:runId/events", (req, res) => {
167
- sseStream(req, res, req.params.runId);
187
+ const { runId } = req.params;
188
+ if (!isValidRunId(runId)) { res.status(400).json({ error: "invalid run id" }); return; }
189
+ sseStream(req, res, runId);
168
190
  });
169
191
 
170
192
  // ----------------------------------------------------------------
@@ -172,6 +194,10 @@ app.get("/api/runs/:runId/events", (req, res) => {
172
194
  // ----------------------------------------------------------------
173
195
  app.get("/api/runs/:runId/log", (req, res) => {
174
196
  const { runId } = req.params;
197
+ if (!isValidRunId(runId)) {
198
+ res.status(400).json({ error: "invalid run id" });
199
+ return;
200
+ }
175
201
 
176
202
  // 1. アクティブセッション(インメモリ)を優先
177
203
  const session = activeSessions.get(runId);
@@ -181,8 +207,8 @@ app.get("/api/runs/:runId/log", (req, res) => {
181
207
  }
182
208
 
183
209
  // 2. 保存済みログファイルにフォールバック
184
- const logFilePath = join(process.cwd(), "logs", `log_${runId}.txt`);
185
- if (existsSync(logFilePath)) {
210
+ const logFilePath = safeLogPath(`log_${runId}.txt`);
211
+ if (logFilePath && existsSync(logFilePath)) {
186
212
  const lines = readFileSync(logFilePath, "utf-8").split("\n").filter((l) => l !== "");
187
213
  res.json({ lines, done: true, exitCode: null });
188
214
  return;
package/server/runs.ts CHANGED
@@ -98,6 +98,7 @@ export function listRuns(): RunSummary[] {
98
98
  }
99
99
 
100
100
  export function getReportPath(runId: string): string | null {
101
+ if (!/^run_\d+$/.test(runId)) return null;
101
102
  const p = path.join(process.cwd(), "logs", `report_${runId}.html`);
102
103
  return fs.existsSync(p) ? p : null;
103
104
  }