Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/lib/voyageai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function embedQuery(
Authorization: `Bearer ${opts.apiKey}`,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});

if (!res.ok) {
Expand Down Expand Up @@ -96,6 +97,7 @@ export async function embedQuery(
Authorization: `Bearer ${opts.apiKey}`,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});

if (!res.ok) {
Expand Down
4 changes: 3 additions & 1 deletion api/src/routes/dataShared/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function buildFilterWhere(filters: unknown): string | null {
}
case "contains": {
const value = String(f.value ?? "");
clauses.push(`${col} LIKE ${sqlString(`%${value}%`)}`);
// Escape SQL LIKE wildcards to prevent pattern injection
const escaped = value.replace(/[%_\\]/g, (ch) => `\\${ch}`);
clauses.push(`${col} LIKE ${sqlString(`%${escaped}%`)} ESCAPE '\\'`);
break;
}
default:
Expand Down
8 changes: 4 additions & 4 deletions api/src/routes/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const graphRoutes = new Hono()
}

const maxEdgesRaw = normalizeIndex(payload.max_edges);
const maxEdges = maxEdgesRaw && maxEdgesRaw > 0 ? maxEdgesRaw : 5000;
const maxEdges = Math.min(maxEdgesRaw && maxEdgesRaw > 0 ? maxEdgesRaw : 5000, 50_000);
const total = filtered.length;
const out = filtered.slice(0, maxEdges);

Expand All @@ -107,8 +107,8 @@ export const graphRoutes = new Hono()
.get("/datasets/:dataset/links/thread/:tweetId", async (c) => {
const { dataset, tweetId } = c.req.param();
try {
const chainLimit = Math.max(1, normalizeIndex(c.req.query("chain_limit")) ?? 300);
const descLimit = Math.max(0, normalizeIndex(c.req.query("desc_limit")) ?? 3000);
const chainLimit = Math.min(10_000, Math.max(1, normalizeIndex(c.req.query("chain_limit")) ?? 300));
const descLimit = Math.min(50_000, Math.max(0, normalizeIndex(c.req.query("desc_limit")) ?? 3000));

const result = await lanceGraphRepo.getThreadEdges(dataset, tweetId, {
chainLimit,
Expand All @@ -128,7 +128,7 @@ export const graphRoutes = new Hono()
.get("/datasets/:dataset/links/quotes/:tweetId", async (c) => {
const { dataset, tweetId } = c.req.param();
try {
const limit = Math.max(1, normalizeIndex(c.req.query("limit")) ?? 2000);
const limit = Math.min(10_000, Math.max(1, normalizeIndex(c.req.query("limit")) ?? 2000));
const result = await lanceGraphRepo.getQuoteEdges(dataset, tweetId, limit);

return c.json({
Expand Down
2 changes: 1 addition & 1 deletion api/src/routes/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const queryRoutes = new Hono()
const indexColumn = resolveIndexColumn(tableColumns);

const perPage = 100;
const page = Math.max(0, normalizeIndex(payload.page) ?? 0);
const page = Math.min(Math.max(0, normalizeIndex(payload.page) ?? 0), 10_000);
const offset = page * perPage;
const sort = payload.sort as JsonRecord | undefined;

Expand Down
5 changes: 4 additions & 1 deletion api/src/routes/resolve-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function resolveRedirect(url: string): Promise<string> {
}

try {
const res = await fetch(url, { redirect: "manual" });
const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5000) });
const location = res.headers.get("location");
return location ?? url;
} catch {
Expand All @@ -47,6 +47,9 @@ export const resolveUrlRoutes = new Hono()
if (!body.urls || !Array.isArray(body.urls)) {
return c.json({ error: "urls array is required" }, 400);
}
if (body.urls.length > 50) {
return c.json({ error: "Maximum 50 URLs per batch" }, 400);
}

const results = await Promise.all(
body.urls.map(async (url) => ({
Expand Down
14 changes: 11 additions & 3 deletions api/src/routes/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ export const searchRoutes = new Hono()
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[NN] ${dataset}/${scope_id} query="${query}":`, message);
return c.json({ error: message }, 500);
// Don't leak internal error details to clients
const safeMessage = message.includes("VOYAGE_API_KEY")
? "Search service configuration error"
: message.includes("not found") || message.includes("missing")
? message
: "Search failed";
return c.json({ error: safeMessage }, 500);
}
})
.get(
Expand Down Expand Up @@ -170,8 +176,10 @@ export const searchRoutes = new Hono()
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[FTS] ${dataset}/${scope_id} query="${query}":`, message);
const status = message.includes("not ready yet") ? 503 : 500;
return c.json({ error: message }, status);
const isNotReady = message.includes("not ready yet");
const status = isNotReady ? 503 : 500;
const safeMessage = isNotReady ? message : "Full-text search failed";
return c.json({ error: safeMessage }, status);
}
},
);
3 changes: 3 additions & 0 deletions latentscope/importers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
"""Data import helpers for external source formats."""

from latentscope.importers.csv_import import load_csv, load_csv_string # noqa: F401
from latentscope.importers.twitterapi_io import fetch_twitterapi_io, load_twitterapi_io_json # noqa: F401
from latentscope.importers.xanalyzer_csv import load_xanalyzer_csv # noqa: F401
Loading