@m8i-51/shoal 0.1.16 → 0.1.17
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
package/server/index.ts
CHANGED
|
@@ -3,11 +3,12 @@ import express from "express";
|
|
|
3
3
|
import { rateLimit } from "express-rate-limit";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { dirname, join, resolve } from "path";
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
|
|
7
7
|
import { listRuns, getReportPath } from "./runs.js";
|
|
8
8
|
import { activeSessions, spawnRun, cancelSession } from "./runner.js";
|
|
9
9
|
import { loadSchedule, saveSchedule, startScheduler, type ScheduleConfig } from "./scheduler.js";
|
|
10
10
|
import { generateDiary, getDiaryPath } from "../framework/diary.js";
|
|
11
|
+
import type { Finding } from "../framework/types.js";
|
|
11
12
|
|
|
12
13
|
function specFilePath(baseUrl: string): string {
|
|
13
14
|
try {
|
|
@@ -128,6 +129,69 @@ app.post("/api/runs/:runId/diary", async (req, res) => {
|
|
|
128
129
|
}
|
|
129
130
|
});
|
|
130
131
|
|
|
132
|
+
// ----------------------------------------------------------------
|
|
133
|
+
// API: Hall of Issues — 全 run の findings を横断取得
|
|
134
|
+
// ----------------------------------------------------------------
|
|
135
|
+
function loadAllFindings(): (Finding & { runId: string })[] {
|
|
136
|
+
const base = resolve(process.cwd(), "findings");
|
|
137
|
+
if (!existsSync(base)) return [];
|
|
138
|
+
const all: (Finding & { runId: string })[] = [];
|
|
139
|
+
for (const runDir of readdirSync(base)) {
|
|
140
|
+
if (!/^run_\d+$/.test(runDir)) continue;
|
|
141
|
+
const dir = join(base, runDir);
|
|
142
|
+
try {
|
|
143
|
+
for (const file of readdirSync(dir)) {
|
|
144
|
+
if (!file.endsWith(".json")) continue;
|
|
145
|
+
try {
|
|
146
|
+
const f: Finding = JSON.parse(readFileSync(join(dir, file), "utf-8"));
|
|
147
|
+
all.push({ ...f, runId: runDir });
|
|
148
|
+
} catch { /* skip */ }
|
|
149
|
+
}
|
|
150
|
+
} catch { /* skip */ }
|
|
151
|
+
}
|
|
152
|
+
return all.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
app.get("/api/findings", (_req, res) => {
|
|
156
|
+
res.json(loadAllFindings());
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.get("/api/findings/export", (_req, res) => {
|
|
160
|
+
const findings = loadAllFindings().map(({ id, title, body, category, agentName, role, timestamp, runId }) => ({
|
|
161
|
+
id, title, body, category, agentName, role, timestamp, runId,
|
|
162
|
+
}));
|
|
163
|
+
const bundle = { version: "1", exportedAt: new Date().toISOString(), source: "shoal", findings };
|
|
164
|
+
res.setHeader("Content-Type", "application/json");
|
|
165
|
+
res.setHeader("Content-Disposition", `attachment; filename="shoal-findings-${Date.now()}.json"`);
|
|
166
|
+
res.json(bundle);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
app.post("/api/findings/proxy-url", async (req, res) => {
|
|
170
|
+
const { url } = req.body as { url?: string };
|
|
171
|
+
if (!url || typeof url !== "string") { res.status(400).json({ error: "url required" }); return; }
|
|
172
|
+
let parsed: URL;
|
|
173
|
+
try {
|
|
174
|
+
parsed = new URL(url);
|
|
175
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("invalid protocol");
|
|
176
|
+
const h = parsed.hostname;
|
|
177
|
+
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h.startsWith("192.168.") || h.startsWith("10.") || h.endsWith(".local")) {
|
|
178
|
+
res.status(400).json({ error: "private urls not allowed" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
res.status(400).json({ error: "invalid url" });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const upstream = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
187
|
+
if (!upstream.ok) { res.status(502).json({ error: "upstream error" }); return; }
|
|
188
|
+
const data = await upstream.json();
|
|
189
|
+
res.json(data);
|
|
190
|
+
} catch {
|
|
191
|
+
res.status(502).json({ error: "failed to fetch url" });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
131
195
|
// ----------------------------------------------------------------
|
|
132
196
|
// API: serve HTML report for a run
|
|
133
197
|
// ----------------------------------------------------------------
|