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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
**Vulnerability:** XSS risk via unsanitized `<` characters in `JSON.stringify` output injected into `<script type="application/ld+json">`.
**Learning:** `JSON.stringify()` does not automatically escape `<` as `\u003c`. If dynamic or unsanitized content is serialized into a `<script>` tag via `dangerouslySetInnerHTML`, an attacker can include `</script>` to break out of the context and inject malicious scripts.
**Prevention:** Always replace `<` with `\u003c` when injecting JSON output into script tags, e.g., `JSON.stringify(data).replace(/</g, '\\u003c')`.

## 2026-06-18 - Native Fetch Timeouts
**Vulnerability:** External fetch calls hanging indefinitely.
**Learning:** The native `fetch` API does not have a default timeout. Without an explicit abort signal, external API calls can hang indefinitely, leading to resource exhaustion, slow page loads, and potential Denial of Service (DoS).
**Prevention:** Always include a timeout signal for external `fetch` calls using `signal: AbortSignal.timeout(TIMEOUT_MS)`.
3 changes: 3 additions & 0 deletions scripts/sync-sources.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Medium

https://github.com/felirami/hypersnaporg/blob/96c282ec0689e64f9bf8d36a2860a19f7331d88d/scripts/sync-sources.mjs#L29

When optional: true is passed and the request times out, AbortSignal.timeout() causes fetch to reject with a TimeoutError before the response status is checked. This bypasses the graceful handling of 404/409 on lines 39-41, causing the entire script to crash even for optional resources like hyper.md or SUMMARY.md when GitHub is slow.

Consider catching AbortError and returning null (or the appropriate empty value) when optional is true, so timeouts don't crash the script.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/sync-sources.mjs around line 29:

When `optional: true` is passed and the request times out, `AbortSignal.timeout()` causes `fetch` to reject with a `TimeoutError` before the response status is checked. This bypasses the graceful handling of 404/409 on lines 39-41, causing the entire script to crash even for optional resources like `hyper.md` or `SUMMARY.md` when GitHub is slow.

Consider catching `AbortError` and returning `null` (or the appropriate empty value) when `optional` is true, so timeouts don't crash the script.

Evidence trail:
scripts/sync-sources.mjs lines 29-49 (fetchGithubJson with AbortSignal.timeout on line 36, 404/409 handling on lines 39-41), lines 51-71 (fetchGithubText, same pattern), lines 346-357 (getRepoTree calls with optional:true, no try/catch), lines 359-374 (getLatestRelease, same), lines 376-444 (buildRepoSnapshot calls fetchGithubText with optional:true, no try/catch), lines 507-509 (main catch handler calls process.exit(1)). AbortSignal.timeout() behavior: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static β€” throws TimeoutError when deadline expires, which rejects the fetch promise before any response is available.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const DOCS_LINK_LIMIT = 80;
const README_HEADING_LIMIT = 10;

const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
const GITHUB_FETCH_TIMEOUT_MS = 15000;

async function fetchGithubJson(url, { optional = false } = {}) {
const response = await fetch(url, {
Expand All @@ -32,6 +33,7 @@ async function fetchGithubJson(url, { optional = false } = {}) {
"X-GitHub-Api-Version": "2022-11-28",
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
},
signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS),
});

if ((response.status === 404 || response.status === 409) && optional) {
Expand All @@ -53,6 +55,7 @@ async function fetchGithubText(url, { optional = false } = {}) {
"X-GitHub-Api-Version": "2022-11-28",
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
},
signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS),
});

if ((response.status === 404 || response.status === 409) && optional) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function getNetworkStatus(): Promise<NetworkStatus> {
headers: {
Accept: "application/json",
},
signal: AbortSignal.timeout(NODE_PROBE_TIMEOUT_MS),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fetch memoization for shared status probes

On /network, src/app/network/page.tsx:30 calls getNetworkStatus() and NetworkStatusGrid calls both getNetworkStatus() and getNodeHealthStatuses() (src/components/network-status.tsx:120-122), with the first known node using the same public /v1/info URL. Next's fetch docs state that passing a signal opts out of render-pass memoization, so when the revalidated cache entry is cold or stale this new signal turns the formerly deduped probe into multiple concurrent requests to the same external node. Please keep timeout handling in a shared cached helper or otherwise preserve memoization so the status page does not amplify load.

Useful? React with πŸ‘Β / πŸ‘Ž.

});

if (!response.ok) {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/snap-market.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SNAP, correctedSnapFdv } from "@/lib/snap";

export const SNAP_MARKET_REVALIDATE = 30;
export const SNAP_MARKET_TIMEOUT_MS = 10_000;

type DexPeriod = {
buys?: number;
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function getSnapMarketData(): Promise<SnapMarketResponse> {
"user-agent": "hypersnap.org market data checker",
},
next: { revalidate: SNAP_MARKET_REVALIDATE },
signal: AbortSignal.timeout(SNAP_MARKET_TIMEOUT_MS),
});

if (!response.ok) {
Expand Down
Loading