Skip to content

fix(telegram): normalize relay feed contracts#2753

Open
shinzoxD wants to merge 2 commits intokoala73:mainfrom
shinzoxD:codex/telegram-feed-contract-fix
Open

fix(telegram): normalize relay feed contracts#2753
shinzoxD wants to merge 2 commits intokoala73:mainfrom
shinzoxD:codex/telegram-feed-contract-fix

Conversation

@shinzoxD
Copy link
Copy Markdown
Contributor

@shinzoxD shinzoxD commented Apr 5, 2026

Summary

  • normalize /api/telegram-feed into the browser Telegram UI contract whether the relay returns messages[] or items[]
  • make normalized item count authoritative for both the edge relay response and listTelegramFeed
  • add a focused regression test that covers both the edge handler and the public intelligence handler

What changed

  • add // @ts-check and typed normalizers in api/telegram-feed.js
  • coerce alternate relay field names (timestampMs / timestamp / ts, sourceUrl / url, channelTitle / channelName / channel)
  • keep ts non-null with a safe ISO fallback so the browser panel never receives null
  • validate edge response URLs to http(s) only and coerce tags / mediaUrls to strings
  • update server/worldmonitor/intelligence/v1/list-telegram-feed.ts to normalize the same relay variants and stop trusting stale relay count

Verification

  • node --import tsx --test tests/telegram-feed-contract.test.mjs
  • npm run typecheck:api
  • node_modules/@biomejs/biome/bin/biome check api/telegram-feed.js server/worldmonitor/intelligence/v1/list-telegram-feed.ts tests/telegram-feed-contract.test.mjs

Closes #2593.

Copilot AI review requested due to automatic review settings April 5, 2026 21:02
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

Someone is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Normalizes Telegram relay payload variants so both the edge /api/telegram-feed endpoint (browser UI contract) and the server intelligence listTelegramFeed handler (public API contract) behave consistently when the relay returns either messages[] or items[], and ensures count is derived from normalized items.

Changes:

  • Added typed normalization in api/telegram-feed.js (field coercion, http(s) URL validation, non-null ISO ts, and authoritative count).
  • Updated server/worldmonitor/intelligence/v1/list-telegram-feed.ts to normalize relay field variants and compute count from mapped messages.
  • Added regression tests covering both edge handler normalization and server handler normalization.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
api/telegram-feed.js Normalizes relay feed responses into the browser Telegram UI contract and adjusts cache TTL based on normalized count.
server/worldmonitor/intelligence/v1/list-telegram-feed.ts Normalizes relay variants into the intelligence API contract and stops trusting relay-provided count.
tests/telegram-feed-contract.test.mjs Adds focused tests to prevent contract drift across edge handler + server handler normalization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +88 to +94
return new Date(value > 1e12 ? value : value * 1000).toISOString();
}
const raw = toString(value).trim();
if (!raw) return EPOCH_ISO;
const numeric = Number(raw);
if (Number.isFinite(numeric) && numeric > 0) {
return new Date(numeric > 1e12 ? numeric : numeric * 1000).toISOString();
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

toIsoTimestamp treats numeric values as milliseconds only when value > 1e12. This misclassifies an exact 1e12 ms timestamp (2001-09-09) as seconds and produces an incorrect far-future ISO string. Consider using >= 1e12 (and the same for the string-numeric branch) so boundary values are interpreted consistently as milliseconds.

Suggested change
return new Date(value > 1e12 ? value : value * 1000).toISOString();
}
const raw = toString(value).trim();
if (!raw) return EPOCH_ISO;
const numeric = Number(raw);
if (Number.isFinite(numeric) && numeric > 0) {
return new Date(numeric > 1e12 ? numeric : numeric * 1000).toISOString();
return new Date(value >= 1e12 ? value : value * 1000).toISOString();
}
const raw = toString(value).trim();
if (!raw) return EPOCH_ISO;
const numeric = Number(raw);
if (Number.isFinite(numeric) && numeric > 0) {
return new Date(numeric >= 1e12 ? numeric : numeric * 1000).toISOString();

Copilot uses AI. Check for mistakes.
Comment on lines +79 to 82
channelName: toText(message.channelName || message.channelTitle || message.channel),
text: toText(message.text),
timestampMs: toTimestampMs(message.timestampMs ?? message.timestamp ?? message.ts),
mediaUrls: Array.isArray(message.mediaUrls) ? message.mediaUrls.map(String) : [],
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

toTimestampMs returns numeric values as-is, so if the relay provides timestamps in seconds (common for ts), timestampMs will be off by 1000x. Since this change now explicitly accepts timestampMs ?? timestamp ?? ts, it would be safer to detect second-based epochs (e.g., < 1e12) and multiply by 1000, similar to the edge handler’s normalization logic.

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 5, 2026

Greptile Summary

This PR normalizes the /api/telegram-feed edge handler and the listTelegramFeed server RPC to tolerate divergent relay field names (messages[]/items[], timestampMs/timestamp/ts, sourceUrl/url, channelTitle/channelName). The edge handler (api/telegram-feed.js) is well-implemented — it has a correct seconds→ms timestamp coercion, an http(s)-only URL allowlist, typed JSDoc, and a test that validates XSS filtering and stale-count correction. However, the server handler (list-telegram-feed.ts) has two functional gaps relative to the edge function that are not caught by the current test suite.

  • api/telegram-feed.js: Clean normalization with toIsoTimestamp (seconds→ms aware), toHttpUrl (strips javascript: etc.), and toStringArray; makes count authoritative from normalized items.
  • list-telegram-feed.ts — timestamp bug: toTimestampMs returns raw numeric values without the value > 1e12 seconds→ms heuristic, silently producing ~1990-era timestamps when the relay sends Unix-second values via the newly-supported timestamp or ts fields.
  • list-telegram-feed.ts — missing URL sanitization: mediaUrls uses only .map(String), leaving javascript: URLs intact — unlike the edge handler which filters to http(s) only.
  • TelegramRelayMessage.mediaUrls is typed string[] despite the relay (and the test fixture) supplying non-string elements; should be unknown[] to match RawTelegramMessage in the edge handler.
  • Test coverage: Both handlers are exercised and the stale-count fix is validated, but no test sends a pure Unix-second timestamp value through listTelegramFeed, so the seconds→ms bug is not caught.

Confidence Score: 2/5

Not safe to merge — the server handler has a silent timestamp data-corruption bug and an unsanitized mediaUrls XSS gap.

The edge handler is solid and the test suite is a genuine improvement, but two P1 issues in list-telegram-feed.ts (wrong timestamp epoch for second-precision values; javascript: URLs leaking through mediaUrls) are not caught by the new tests and will affect production.

server/worldmonitor/intelligence/v1/list-telegram-feed.ts requires fixes to toTimestampMs (seconds→ms heuristic) and mediaUrls sanitization before merging.

Important Files Changed

Filename Overview
api/telegram-feed.js Well-structured normalization layer with correct seconds→ms timestamp coercion, http(s)-only URL validation, field aliasing for all relay variants, and authoritative count from normalized items.
server/worldmonitor/intelligence/v1/list-telegram-feed.ts Field aliasing and stale-count fix are good, but toTimestampMs lacks seconds→ms conversion (silent data corruption) and mediaUrls allows javascript: URLs through (XSS risk).
tests/telegram-feed-contract.test.mjs Good bilateral coverage for both handlers including stale-count correction and XSS URL filtering in the edge path, but missing a pure Unix-second timestamp test for the listTelegramFeed path.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Edge as api/telegram-feed.js (Edge)
    participant RPC as listTelegramFeed (Server RPC)
    participant Relay as Relay Service

    Browser->>Edge: GET /api/telegram-feed?limit=50
    Edge->>Relay: GET /telegram/feed?limit=50
    Relay-->>Edge: messages[]|items[], count (stale)
    Edge->>Edge: normalizeTelegramFeed()<br/>toIsoTimestamp (s→ms ✓)<br/>toHttpUrl (strips javascript: ✓)
    Edge-->>Browser: items[], count=items.length

    Browser->>RPC: ListTelegramFeed RPC
    RPC->>Relay: GET /telegram/feed?limit=N
    Relay-->>RPC: messages[]|items[], count (stale)
    RPC->>RPC: toTimestampMs (no s→ms ⚠️)<br/>.map(String) on mediaUrls (no URL filter ⚠️)
    RPC-->>Browser: messages[], count=messages.length
Loading

Comments Outside Diff (1)

  1. server/worldmonitor/intelligence/v1/list-telegram-feed.ts, line 33-40 (link)

    P1 toTimestampMs missing seconds → milliseconds conversion

    This function returns raw numeric values without the seconds-to-milliseconds heuristic that the edge function's toIsoTimestamp uses (value > 1e12 ? value : value * 1000). The PR explicitly adds message.timestampMs ?? message.timestamp ?? message.ts as the multi-field fallback, making it likely the relay will supply a timestamp value in Unix seconds (e.g. 1744000000). When that lands in toTimestampMs, it is returned as-is; clients then interpret 1744000000 ms as roughly January 1990, not April 2025.

    The edge function in api/telegram-feed.js gets this right — toIsoTimestamp converts seconds to milliseconds via value > 1e12 ? value : value * 1000. toTimestampMs needs the same guard:

Reviews (1): Last reviewed commit: "fix(telegram): normalize relay feed cont..." | Re-trigger Greptile

channelName: toText(message.channelName || message.channelTitle || message.channel),
text: toText(message.text),
timestampMs: toTimestampMs(message.timestampMs ?? message.timestamp ?? message.ts),
mediaUrls: Array.isArray(message.mediaUrls) ? message.mediaUrls.map(String) : [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 mediaUrls not sanitized against non-http(s) schemes

The server handler converts mediaUrls with only .map(String), so a relay-supplied javascript:evil() URL passes through unchanged into the response. The edge function in api/telegram-feed.js guards against this explicitly by using toStringArray(message.mediaUrls, toHttpUrl), which strips any URL whose protocol is not http: or https:.

If downstream clients render these URLs in <img src>, <a href>, or similar attributes, the unfiltered values are a XSS vector. Apply the same scheme allowlist here:

Suggested change
mediaUrls: Array.isArray(message.mediaUrls) ? message.mediaUrls.map(String) : [],
mediaUrls: Array.isArray(message.mediaUrls) ? message.mediaUrls.map(String).filter(u => { try { const p = new URL(u); return p.protocol === 'http:' || p.protocol === 'https:'; } catch { return false; } }) : [],

(Or, better, extract the toHttpUrl helper into a shared module and reuse it in both handlers.)

timestamp?: string | number;
timestampMs?: string | number;
ts?: string | number;
mediaUrls?: string[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 mediaUrls typed as string[] but relay sends mixed types

The TelegramRelayMessage interface declares mediaUrls?: string[], yet the PR's own test fixture sends mediaUrls: [91, 'https://cdn.example.com/chart.png'] — a mixed number | string array. The code handles this correctly at runtime via .map(String), but the interface type is misleading and will cause TypeScript to suppress the coercion as unnecessary.

The edge function's RawTelegramMessage correctly models this as mediaUrls?: unknown[]. Matching that here makes the intent explicit:

Suggested change
mediaUrls?: string[];
mediaUrls?: unknown[];

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aeef13cc30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +194 to +198
const normalized = normalizeTelegramFeed(parsed);
if (normalized.count === 0) {
cacheControl = 'public, max-age=0, s-maxage=15, stale-while-revalidate=10';
}
return buildRelayResponse(response, JSON.stringify(normalized), {
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 upstream error payloads on relay failures

This branch normalizes and rewrites any JSON response body before checking response.ok, so a relay 4xx/5xx like {"error":"rate_limited"} is returned with the same HTTP status but with a feed-shaped body (count/items) and no error details. That regresses error semantics for clients that inspect response JSON for actionable diagnostics/backoff reasons, and it makes failures look like an empty feed payload instead of a real upstream error.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trust:safe Brin: contributor trust score safe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram feed contract mismatch between edge relay/UI and intelligence API

2 participants