diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3362806..195a952 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"islandflow-0e3","title":"Fix PR 23 CI failures","description":"PR 23 is failing the Forgejo CI Validate workflow. Reproduce the failing gates locally, fix the underlying formatting/lint/typecheck/test/build issues, update the PR branch, and confirm the remote check passes.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-14T19:35:07Z","created_by":"dirtydishes","updated_at":"2026-06-14T19:37:01Z","started_at":"2026-06-14T19:35:12Z","closed_at":"2026-06-14T19:37:01Z","close_reason":"Local Validate workflow passes after applying formatter output and syncing the Docker workspace snapshot.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9w7","title":"Allow local dev origins on hosted API","description":"Local bun run dev:web and desktop-local point at the hosted API, but browser requests from http://127.0.0.1:3000 are blocked because the API omits CORS headers and returns 404 for OPTIONS preflight. Add API-side CORS handling, validate local web/desktop browser access, and deploy the API fix.","acceptance_criteria":"API responses include Access-Control-Allow-Origin for allowed local/dev origins; OPTIONS preflight succeeds; bun run dev:web reaches hosted REST/WS endpoints from a browser; bun run dev:desktop local mode reaches the backend through the local web UI; tests/build pass; fix is deployed to api.flow.deltaisland.io.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T15:04:19Z","created_by":"dirtydishes","updated_at":"2026-06-13T15:29:42Z","started_at":"2026-06-13T15:04:26Z","closed_at":"2026-06-13T15:29:42Z","close_reason":"Hosted API now reflects allowed local dev origins and handles OPTIONS preflight; local web and desktop dev runners both reach https://api.flow.deltaisland.io; API tests, typecheck, and web build passed.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xkq","title":"Rebuild production dashboard options news around mock9 aesthetic","description":"Reconstruct the production web UI for Dashboard, Options, and News around the mock9 through mock12 dense terminal aesthetic while preserving production data subscriptions, drawers, virtualization, route helpers, redirects, and validation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T14:07:34Z","created_by":"dirtydishes","updated_at":"2026-06-13T14:26:46Z","started_at":"2026-06-13T14:07:53Z","closed_at":"2026-06-13T14:26:46Z","close_reason":"Rebuilt Dashboard, Options, and News around the dense mock9 to mock12 production aesthetic; tests and build passed, and Browser visual inspection was documented as blocked by the unavailable in-app browser backend.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-u45","title":"Patch CVE-related dependency and Docker image findings","description":"Address Forgejo issues #15, #18, and #19 by upgrading the vulnerable tmp dependency resolution and moving Bun Docker images off the vulnerable oven/bun:1.3.11 base image with patched OpenSSL packages during image build.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T23:21:29Z","created_by":"dirtydishes","updated_at":"2026-06-12T23:23:27Z","started_at":"2026-06-12T23:22:16Z","closed_at":"2026-06-12T23:23:27Z","close_reason":"Patched Forgejo #15/#18 tmp CVE by resolving tmp@0.2.7, updated Bun Docker images and OpenSSL package upgrade layers for #19, and validated with bun audit, tests, web build, docker workspace check, and replacement image manifest inspection.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/deployment/docker/workspace-root/bun.lock b/deployment/docker/workspace-root/bun.lock index 9b60caa..0b7d3ab 100644 --- a/deployment/docker/workspace-root/bun.lock +++ b/deployment/docker/workspace-root/bun.lock @@ -176,7 +176,7 @@ "@electron/node-gyp": "^10.2.0-electron.2", "postcss": "^8.5.15", "tar": "^7.5.15", - "tmp": "^0.2.5", + "tmp": "^0.2.6", }, "packages": { "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], @@ -1175,7 +1175,7 @@ "terser-webpack-plugin": ["terser-webpack-plugin@5.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], diff --git a/deployment/docker/workspace-root/package.json b/deployment/docker/workspace-root/package.json index 7dc2533..a7789a7 100644 --- a/deployment/docker/workspace-root/package.json +++ b/deployment/docker/workspace-root/package.json @@ -39,7 +39,7 @@ "overrides": { "postcss": "^8.5.15", "tar": "^7.5.15", - "tmp": "^0.2.5", + "tmp": "^0.2.6", "@electron/node-gyp": "^10.2.0-electron.2" }, "dependencies": { diff --git a/services/api/src/index.ts b/services/api/src/index.ts index cdfad6e..e450e19 100644 --- a/services/api/src/index.ts +++ b/services/api/src/index.ts @@ -1382,587 +1382,597 @@ const run = async () => { return jsonResponse({ status: "ok" }); } - if (req.method === "GET" && url.pathname === "/admin/synthetic/status") { - const authError = authenticateSyntheticAdminRequest(req); - if (authError) { - return authError; + if (req.method === "GET" && url.pathname === "/admin/synthetic/status") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + return jsonResponse(buildSyntheticStatusBody()); } - return jsonResponse(buildSyntheticStatusBody()); - } - if (req.method === "GET" && url.pathname === "/admin/synthetic/control") { - const authError = authenticateSyntheticAdminRequest(req); - if (authError) { - return authError; + if (req.method === "GET" && url.pathname === "/admin/synthetic/control") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + return jsonResponse({ control: syntheticControl }); } - return jsonResponse({ control: syntheticControl }); - } - if (req.method === "PUT" && url.pathname === "/admin/synthetic/control") { - const authError = authenticateSyntheticAdminRequest(req); - if (authError) { - return authError; + if (req.method === "PUT" && url.pathname === "/admin/synthetic/control") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + try { + const payload = SyntheticControlStateSchema.parse(await readJsonBody(req)); + syntheticControl = await writeSyntheticControlState(syntheticControlKv, payload); + return jsonResponse({ + control: syntheticControl, + derived: buildSyntheticDerivedStatus( + Date.now(), + syntheticControl, + syntheticProfileHits + ) + }); + } catch (error) { + return jsonResponse( + { + error: "invalid synthetic control payload", + detail: getErrorMessage(error) + }, + 400 + ); + } } - try { - const payload = SyntheticControlStateSchema.parse(await readJsonBody(req)); - syntheticControl = await writeSyntheticControlState(syntheticControlKv, payload); - return jsonResponse({ - control: syntheticControl, - derived: buildSyntheticDerivedStatus(Date.now(), syntheticControl, syntheticProfileHits) - }); - } catch (error) { - return jsonResponse( - { - error: "invalid synthetic control payload", - detail: getErrorMessage(error) - }, - 400 - ); - } - } - if (req.method === "GET" && url.pathname === "/prints/options") { - try { + if (req.method === "GET" && url.pathname === "/prints/options") { + try { + const limit = parseLimit(url.searchParams.get("limit")); + const source = parseReplaySource(url) ?? undefined; + const { storageFilters } = parseOptionPrintQuery(url); + const data = await fetchRecentOptionPrints(clickhouse, limit, source, storageFilters); + return jsonResponse({ data }); + } catch (error) { + return jsonResponse( + { + error: "invalid options query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } + } + + if (req.method === "GET" && url.pathname === "/nbbo/options") { const limit = parseLimit(url.searchParams.get("limit")); const source = parseReplaySource(url) ?? undefined; - const { storageFilters } = parseOptionPrintQuery(url); - const data = await fetchRecentOptionPrints(clickhouse, limit, source, storageFilters); + const data = await fetchRecentOptionNBBO(clickhouse, limit, source); return jsonResponse({ data }); - } catch (error) { - return jsonResponse( - { - error: "invalid options query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); } - } - if (req.method === "GET" && url.pathname === "/nbbo/options") { - const limit = parseLimit(url.searchParams.get("limit")); - const source = parseReplaySource(url) ?? undefined; - const data = await fetchRecentOptionNBBO(clickhouse, limit, source); - return jsonResponse({ data }); - } - - if (req.method === "GET" && url.pathname === "/prints/equities") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentEquityPrints(clickhouse, limit); - return jsonResponse({ data }); - } - - if (req.method === "GET" && url.pathname === "/prints/equities/range") { - try { - const { underlyingId, startTs, endTs, limit } = parseEquityPrintRangeParams(url); - const data = await fetchEquityPrintsRange( - clickhouse, - underlyingId, - startTs, - endTs, - limit - ); + if (req.method === "GET" && url.pathname === "/prints/equities") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentEquityPrints(clickhouse, limit); return jsonResponse({ data }); - } catch (error) { - return jsonResponse( - { - error: "invalid equity range query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); } - } - if (req.method === "GET" && url.pathname === "/quotes/equities") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentEquityQuotes(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/prints/equities/range") { + try { + const { underlyingId, startTs, endTs, limit } = parseEquityPrintRangeParams(url); + const data = await fetchEquityPrintsRange( + clickhouse, + underlyingId, + startTs, + endTs, + limit + ); + return jsonResponse({ data }); + } catch (error) { + return jsonResponse( + { + error: "invalid equity range query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } + } - if (req.method === "GET" && url.pathname === "/candles/equities") { - try { - const { underlyingId, intervalMs, startTs, endTs, limit, useCache } = - parseCandleParams(url); - if (useCache && redis && redis.isOpen) { - const cached = await fetchEquityCandlesFromCache( - redis, + if (req.method === "GET" && url.pathname === "/quotes/equities") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentEquityQuotes(clickhouse, limit); + return jsonResponse({ data }); + } + + if (req.method === "GET" && url.pathname === "/candles/equities") { + try { + const { underlyingId, intervalMs, startTs, endTs, limit, useCache } = + parseCandleParams(url); + if (useCache && redis && redis.isOpen) { + const cached = await fetchEquityCandlesFromCache( + redis, + underlyingId, + intervalMs, + startTs, + endTs + ); + if (cached.length > 0) { + return jsonResponse({ data: cached }); + } + } + + const data = await fetchEquityCandlesRange( + clickhouse, underlyingId, intervalMs, startTs, - endTs + endTs, + limit + ); + return jsonResponse({ data }); + } catch (error) { + return jsonResponse( + { + error: "invalid candle query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 ); - if (cached.length > 0) { - return jsonResponse({ data: cached }); - } } + } - const data = await fetchEquityCandlesRange( - clickhouse, - underlyingId, - intervalMs, - startTs, - endTs, - limit - ); + if (req.method === "GET" && url.pathname === "/joins/equities") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentEquityPrintJoins(clickhouse, limit); return jsonResponse({ data }); - } catch (error) { - return jsonResponse( - { - error: "invalid candle query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); } - } - if (req.method === "GET" && url.pathname === "/joins/equities") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentEquityPrintJoins(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/dark/inferred") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentInferredDark(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/dark/inferred") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentInferredDark(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/flow/packets") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentFlowPackets(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/flow/packets") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentFlowPackets(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/flow/smart-money") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentSmartMoneyEvents(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/flow/smart-money") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentSmartMoneyEvents(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/flow/classifier-hits") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentClassifierHits(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/flow/classifier-hits") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentClassifierHits(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/flow/alerts") { + const limit = parseLimit(url.searchParams.get("limit")); + const data = await fetchRecentAlerts(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/flow/alerts") { - const limit = parseLimit(url.searchParams.get("limit")); - const data = await fetchRecentAlerts(clickhouse, limit); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/news") { + const limit = parseLimit(url.searchParams.get("limit") ?? "100"); + const data = await fetchRecentNews(clickhouse, limit); + return jsonResponse({ data }); + } - if (req.method === "GET" && url.pathname === "/news") { - const limit = parseLimit(url.searchParams.get("limit") ?? "100"); - const data = await fetchRecentNews(clickhouse, limit); - return jsonResponse({ data }); - } - - if (req.method === "GET" && isAlertContextPath(url.pathname)) { - try { - const traceId = parseAlertContextTraceIdPath(url.pathname); - if (traceId === null) { - return jsonResponse({ error: "not found" }, 404); + if (req.method === "GET" && isAlertContextPath(url.pathname)) { + try { + const traceId = parseAlertContextTraceIdPath(url.pathname); + if (traceId === null) { + return jsonResponse({ error: "not found" }, 404); + } + const data = await fetchAlertContextByTraceId(clickhouse, traceId); + return jsonResponse(data); + } catch (error) { + return jsonResponse( + { + error: "invalid alert context query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); } - const data = await fetchAlertContextByTraceId(clickhouse, traceId); - return jsonResponse(data); - } catch (error) { - return jsonResponse( - { - error: "invalid alert context query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); } - } - if (req.method === "GET" && url.pathname === "/history/options") { - try { + if (req.method === "GET" && url.pathname === "/history/options") { + try { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const source = parseReplaySource(url) ?? undefined; + const { storageFilters } = parseOptionPrintQuery(url); + const data = await fetchOptionPrintsBefore( + clickhouse, + beforeTs, + beforeSeq, + limit, + source, + storageFilters + ); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })) + ); + } catch (error) { + return jsonResponse( + { + error: "invalid options history query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } + } + + if (req.method === "GET" && url.pathname === "/history/nbbo") { const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); const source = parseReplaySource(url) ?? undefined; - const { storageFilters } = parseOptionPrintQuery(url); - const data = await fetchOptionPrintsBefore( + const data = await fetchOptionNBBOBefore(clickhouse, beforeTs, beforeSeq, limit, source); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && url.pathname === "/history/equities") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchEquityPrintsBefore( clickhouse, beforeTs, beforeSeq, limit, - source, - storageFilters + parseLiveEquityPrintFilters(url) ); return jsonResponse( buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })) ); - } catch (error) { + } + + if (req.method === "GET" && url.pathname === "/history/equity-quotes") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchEquityQuotesBefore(clickhouse, beforeTs, beforeSeq, limit); return jsonResponse( - { - error: "invalid options history query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 + buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })) ); } - } - if (req.method === "GET" && url.pathname === "/history/nbbo") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const source = parseReplaySource(url) ?? undefined; - const data = await fetchOptionNBBOBefore(clickhouse, beforeTs, beforeSeq, limit, source); - return jsonResponse(buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq }))); - } - - if (req.method === "GET" && url.pathname === "/history/equities") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchEquityPrintsBefore( - clickhouse, - beforeTs, - beforeSeq, - limit, - parseLiveEquityPrintFilters(url) - ); - return jsonResponse(buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq }))); - } - - if (req.method === "GET" && url.pathname === "/history/equity-quotes") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchEquityQuotesBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse(buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq }))); - } - - if (req.method === "GET" && url.pathname === "/history/equity-joins") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchEquityPrintJoinsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/flow") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchFlowPacketsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/smart-money") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchSmartMoneyEventsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/classifier-hits") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchClassifierHitsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/alerts") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchAlertsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/inferred-dark") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchInferredDarkBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && url.pathname === "/history/news") { - const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); - const data = await fetchNewsBefore(clickhouse, beforeTs, beforeSeq, limit); - return jsonResponse( - buildHistoryResponse(data, (item) => ({ ts: item.published_ts, seq: item.seq })) - ); - } - - if (req.method === "GET" && /^\/flow\/packets\/[^/]+$/.test(url.pathname)) { - const id = decodeURIComponent(url.pathname.slice("/flow/packets/".length)); - const data = await fetchFlowPacketById(clickhouse, id); - return jsonResponse({ data }); - } - - if (req.method === "GET" && /^\/flow\/alerts\/[^/]+\/context$/.test(url.pathname)) { - const traceId = decodeURIComponent( - url.pathname.slice("/flow/alerts/".length, -"/context".length) - ).trim(); - if (!traceId || traceId.length > 512) { - return jsonResponse({ error: "invalid alert trace id" }, 400); - } - const data = await fetchAlertContextByTraceId(clickhouse, traceId); - return jsonResponse(data); - } - - if (req.method === "GET" && url.pathname === "/option-prints/by-trace") { - const traceIds = url.searchParams.getAll("trace_id"); - const data = await fetchOptionPrintsByTraceIds(clickhouse, traceIds); - return jsonResponse({ data }); - } - - if (req.method === "POST" && url.pathname === "/lookup/options-support") { - try { - const body = optionsSupportLookupSchema.parse(await readJsonBody(req)); - const packets = await fetchFlowPacketsByMemberTraceIds(clickhouse, body.trace_ids); - const packetIds = packets.map((packet) => packet.id); - const [smartMoney, classifierHits, nbboByTraceId] = await Promise.all([ - fetchSmartMoneyEventsByPacketIds(clickhouse, packetIds), - fetchClassifierHitsByPacketIds(clickhouse, packetIds), - fetchNearestOptionNBBOForPrints(clickhouse, body.nbbo_context) - ]); - return jsonResponse({ - packets, - smart_money: smartMoney, - classifier_hits: classifierHits, - nbbo_by_trace_id: nbboByTraceId - }); - } catch (error) { + if (req.method === "GET" && url.pathname === "/history/equity-joins") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchEquityPrintJoinsBefore(clickhouse, beforeTs, beforeSeq, limit); return jsonResponse( - { - error: "invalid options support lookup", - detail: error instanceof Error ? error.message : String(error) - }, - 400 + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) ); } - } - if (req.method === "GET" && url.pathname === "/equity-joins/by-id") { - const ids = url.searchParams.getAll("id"); - const data = await fetchEquityPrintJoinsByIds(clickhouse, ids); - return jsonResponse({ data }); - } + if (req.method === "GET" && url.pathname === "/history/flow") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchFlowPacketsBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) + ); + } - if (req.method === "GET" && url.pathname === "/replay/options") { - try { + if (req.method === "GET" && url.pathname === "/history/smart-money") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchSmartMoneyEventsBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && url.pathname === "/history/classifier-hits") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchClassifierHitsBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && url.pathname === "/history/alerts") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchAlertsBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && url.pathname === "/history/inferred-dark") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchInferredDarkBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && url.pathname === "/history/news") { + const { beforeTs, beforeSeq, limit } = parseBeforeParams(url); + const data = await fetchNewsBefore(clickhouse, beforeTs, beforeSeq, limit); + return jsonResponse( + buildHistoryResponse(data, (item) => ({ ts: item.published_ts, seq: item.seq })) + ); + } + + if (req.method === "GET" && /^\/flow\/packets\/[^/]+$/.test(url.pathname)) { + const id = decodeURIComponent(url.pathname.slice("/flow/packets/".length)); + const data = await fetchFlowPacketById(clickhouse, id); + return jsonResponse({ data }); + } + + if (req.method === "GET" && /^\/flow\/alerts\/[^/]+\/context$/.test(url.pathname)) { + const traceId = decodeURIComponent( + url.pathname.slice("/flow/alerts/".length, -"/context".length) + ).trim(); + if (!traceId || traceId.length > 512) { + return jsonResponse({ error: "invalid alert trace id" }, 400); + } + const data = await fetchAlertContextByTraceId(clickhouse, traceId); + return jsonResponse(data); + } + + if (req.method === "GET" && url.pathname === "/option-prints/by-trace") { + const traceIds = url.searchParams.getAll("trace_id"); + const data = await fetchOptionPrintsByTraceIds(clickhouse, traceIds); + return jsonResponse({ data }); + } + + if (req.method === "POST" && url.pathname === "/lookup/options-support") { + try { + const body = optionsSupportLookupSchema.parse(await readJsonBody(req)); + const packets = await fetchFlowPacketsByMemberTraceIds(clickhouse, body.trace_ids); + const packetIds = packets.map((packet) => packet.id); + const [smartMoney, classifierHits, nbboByTraceId] = await Promise.all([ + fetchSmartMoneyEventsByPacketIds(clickhouse, packetIds), + fetchClassifierHitsByPacketIds(clickhouse, packetIds), + fetchNearestOptionNBBOForPrints(clickhouse, body.nbbo_context) + ]); + return jsonResponse({ + packets, + smart_money: smartMoney, + classifier_hits: classifierHits, + nbbo_by_trace_id: nbboByTraceId + }); + } catch (error) { + return jsonResponse( + { + error: "invalid options support lookup", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } + } + + if (req.method === "GET" && url.pathname === "/equity-joins/by-id") { + const ids = url.searchParams.getAll("id"); + const data = await fetchEquityPrintJoinsByIds(clickhouse, ids); + return jsonResponse({ data }); + } + + if (req.method === "GET" && url.pathname === "/replay/options") { + try { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const source = parseReplaySource(url) ?? undefined; + const { storageFilters } = parseOptionPrintQuery(url); + const data = await fetchOptionPrintsAfter( + clickhouse, + afterTs, + afterSeq, + limit, + source, + storageFilters + ); + const last = data.at(-1); + const next = last ? { ts: last.ts, seq: last.seq } : null; + return jsonResponse({ data, next }); + } catch (error) { + return jsonResponse( + { + error: "invalid options replay query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } + } + + if (req.method === "GET" && url.pathname === "/replay/nbbo") { const { afterTs, afterSeq, limit } = parseReplayParams(url); const source = parseReplaySource(url) ?? undefined; - const { storageFilters } = parseOptionPrintQuery(url); - const data = await fetchOptionPrintsAfter( - clickhouse, - afterTs, - afterSeq, - limit, - source, - storageFilters - ); + const data = await fetchOptionNBBOAfter(clickhouse, afterTs, afterSeq, limit, source); const last = data.at(-1); const next = last ? { ts: last.ts, seq: last.seq } : null; return jsonResponse({ data, next }); - } catch (error) { - return jsonResponse( - { - error: "invalid options replay query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); } - } - if (req.method === "GET" && url.pathname === "/replay/nbbo") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const source = parseReplaySource(url) ?? undefined; - const data = await fetchOptionNBBOAfter(clickhouse, afterTs, afterSeq, limit, source); - const last = data.at(-1); - const next = last ? { ts: last.ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/equities") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchEquityPrintsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/equity-quotes") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchEquityQuotesAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/equity-candles") { - try { - const { underlyingId, intervalMs, afterTs, afterSeq, limit } = - parseCandleReplayParams(url); - const data = await fetchEquityCandlesAfter( - clickhouse, - underlyingId, - intervalMs, - afterTs, - afterSeq, - limit - ); + if (req.method === "GET" && url.pathname === "/replay/equities") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchEquityPrintsAfter(clickhouse, afterTs, afterSeq, limit); const last = data.at(-1); const next = last ? { ts: last.ts, seq: last.seq } : null; return jsonResponse({ data, next }); - } catch (error) { - return jsonResponse( - { - error: "invalid candle replay query", - detail: error instanceof Error ? error.message : String(error) - }, - 400 - ); - } - } - - if (req.method === "GET" && url.pathname === "/replay/equity-joins") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchEquityPrintJoinsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/inferred-dark") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchInferredDarkAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/flow") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchFlowPacketsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/smart-money") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchSmartMoneyEventsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/classifier-hits") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchClassifierHitsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/replay/alerts") { - const { afterTs, afterSeq, limit } = parseReplayParams(url); - const data = await fetchAlertsAfter(clickhouse, afterTs, afterSeq, limit); - const last = data.at(-1); - const next = last ? { ts: last.source_ts, seq: last.seq } : null; - return jsonResponse({ data, next }); - } - - if (req.method === "GET" && url.pathname === "/ws/options") { - if (serverRef.upgrade(req, { data: { channel: "options" } })) { - return new Response(null, { status: 101 }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/options-nbbo") { - if (serverRef.upgrade(req, { data: { channel: "options-nbbo" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/equity-quotes") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchEquityQuotesAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/equities") { - if (serverRef.upgrade(req, { data: { channel: "equities" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/equity-candles") { + try { + const { underlyingId, intervalMs, afterTs, afterSeq, limit } = + parseCandleReplayParams(url); + const data = await fetchEquityCandlesAfter( + clickhouse, + underlyingId, + intervalMs, + afterTs, + afterSeq, + limit + ); + const last = data.at(-1); + const next = last ? { ts: last.ts, seq: last.seq } : null; + return jsonResponse({ data, next }); + } catch (error) { + return jsonResponse( + { + error: "invalid candle replay query", + detail: error instanceof Error ? error.message : String(error) + }, + 400 + ); + } } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/equity-candles") { - if (serverRef.upgrade(req, { data: { channel: "equity-candles" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/equity-joins") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchEquityPrintJoinsAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/equity-quotes") { - if (serverRef.upgrade(req, { data: { channel: "equity-quotes" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/inferred-dark") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchInferredDarkAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/equity-joins") { - if (serverRef.upgrade(req, { data: { channel: "equity-joins" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/flow") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchFlowPacketsAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/inferred-dark") { - if (serverRef.upgrade(req, { data: { channel: "inferred-dark" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/smart-money") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchSmartMoneyEventsAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/flow") { - if (serverRef.upgrade(req, { data: { channel: "flow" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/classifier-hits") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchClassifierHitsAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } - - if (req.method === "GET" && url.pathname === "/ws/classifier-hits") { - if (serverRef.upgrade(req, { data: { channel: "classifier-hits" } })) { - return new Response(null, { status: 101 }); + if (req.method === "GET" && url.pathname === "/replay/alerts") { + const { afterTs, afterSeq, limit } = parseReplayParams(url); + const data = await fetchAlertsAfter(clickhouse, afterTs, afterSeq, limit); + const last = data.at(-1); + const next = last ? { ts: last.source_ts, seq: last.seq } : null; + return jsonResponse({ data, next }); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } + if (req.method === "GET" && url.pathname === "/ws/options") { + if (serverRef.upgrade(req, { data: { channel: "options" } })) { + return new Response(null, { status: 101 }); + } - if (req.method === "GET" && url.pathname === "/ws/smart-money") { - if (serverRef.upgrade(req, { data: { channel: "smart-money" } })) { - return new Response(null, { status: 101 }); + return jsonResponse({ error: "websocket upgrade failed" }, 400); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } + if (req.method === "GET" && url.pathname === "/ws/options-nbbo") { + if (serverRef.upgrade(req, { data: { channel: "options-nbbo" } })) { + return new Response(null, { status: 101 }); + } - if (req.method === "GET" && url.pathname === "/ws/alerts") { - if (serverRef.upgrade(req, { data: { channel: "alerts" } })) { - return new Response(null, { status: 101 }); + return jsonResponse({ error: "websocket upgrade failed" }, 400); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } + if (req.method === "GET" && url.pathname === "/ws/equities") { + if (serverRef.upgrade(req, { data: { channel: "equities" } })) { + return new Response(null, { status: 101 }); + } - if (req.method === "GET" && url.pathname === "/ws/live") { - if (serverRef.upgrade(req, { data: { channel: "live" } })) { - return new Response(null, { status: 101 }); + return jsonResponse({ error: "websocket upgrade failed" }, 400); } - return jsonResponse({ error: "websocket upgrade failed" }, 400); - } + if (req.method === "GET" && url.pathname === "/ws/equity-candles") { + if (serverRef.upgrade(req, { data: { channel: "equity-candles" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/equity-quotes") { + if (serverRef.upgrade(req, { data: { channel: "equity-quotes" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/equity-joins") { + if (serverRef.upgrade(req, { data: { channel: "equity-joins" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/inferred-dark") { + if (serverRef.upgrade(req, { data: { channel: "inferred-dark" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/flow") { + if (serverRef.upgrade(req, { data: { channel: "flow" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/classifier-hits") { + if (serverRef.upgrade(req, { data: { channel: "classifier-hits" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/smart-money") { + if (serverRef.upgrade(req, { data: { channel: "smart-money" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/alerts") { + if (serverRef.upgrade(req, { data: { channel: "alerts" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } + + if (req.method === "GET" && url.pathname === "/ws/live") { + if (serverRef.upgrade(req, { data: { channel: "live" } })) { + return new Response(null, { status: 101 }); + } + + return jsonResponse({ error: "websocket upgrade failed" }, 400); + } return jsonResponse({ error: "not found" }, 404); }; diff --git a/services/api/tests/cors.test.ts b/services/api/tests/cors.test.ts index e10d64d..f730e88 100644 --- a/services/api/tests/cors.test.ts +++ b/services/api/tests/cors.test.ts @@ -74,8 +74,6 @@ describe("api cors helpers", () => { expect(response.status).toBe(204); expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:3100"); expect(response.headers.get("access-control-allow-methods")).toContain("POST"); - expect(response.headers.get("access-control-allow-headers")).toBe( - "content-type,authorization" - ); + expect(response.headers.get("access-control-allow-headers")).toBe("content-type,authorization"); }); });