Meshes
A mesh is a named bundle of sources that fetch in parallel and merge into one feed. Point a mesh at several blogs, releases pages, and RSS feeds, and get back a single newest-first feed in any output format.
The Mesh type lives in @neurowire/core (packages/core/src/model.ts); fetching and merging live in fetchMesh (packages/ingest/src/mesh.ts).
Shape
A mesh is a name plus a list of sources, each a display name and a url (a feed URL or a website Neurowire can ingest), with optional per-source request headers:
export const MeshSourceSchema = z.object({
name: z.string(),
url: z.string(),
headers: z.record(z.string(), z.string()).optional(),
})
export const MeshSchema = z.object({ name: z.string(), sources: z.array(MeshSourceSchema) }){
"name": "AI News",
"sources": [
{ "name": "Claude Code Releases", "url": "https://github.com/anthropics/claude-code/releases.atom" },
{ "name": "Claude Blog", "url": "https://claude.com/blog" },
{ "name": "Simon Willison", "url": "https://simonwillison.net/atom/everything/" }
]
}parseMesh(data) validates an unknown value into a Mesh.
How fetching and merging work
fetchMesh(mesh, options) fetches every source in parallel (Promise.allSettled) and then merges the results into one feed via mergeFeeds:
- Each entry is tagged by source (the source name carries through), so a rendered page or feed can show where each item came from.
- Entries are deduplicated (the stable entry ids make the same article from two sources collapse into one).
- The merged feed is sorted newest-first.
- An optional
limitkeeps only the newest N merged entries.
Partial-failure semantics
A mesh is resilient by design. A source that fails to fetch is logged and skipped, never fatal:
- Each failure goes through
onSourceError(source, error), which by default writes a one-line warning to stderr with the source name and a short reason. - The mesh still merges and returns whatever sources succeeded.
- It throws only when every source fails:
Mesh "<name>": no sources could be fetched.
You can pass a custom onSourceError handler to silence or redirect those warnings.
import { fetchMesh } from '@neurowire/ingest'
const feed = await fetchMesh(mesh, {
limit: 50,
onSourceError: (source, err) => log.warn(`skip ${source.name}: ${err}`),
})FetchMeshOptions also forwards the shared fetch tuning (signal, cache, timeoutMs, retries, backoffMs) to every source. A single cache is shared across all sources in the mesh. See Fetching for what those do.
Private sources: per-source headers
A source can carry request headers, sent only when fetching that source. That is how a mesh reads a private feed: a GitHub token for a private repository's releases feed, a bearer token for an internal API, a basic-auth header for a password-protected RSS endpoint.
{
"name": "Internal",
"sources": [
{
"name": "Private releases",
"url": "https://api.github.com/repos/acme/private/releases",
"headers": { "authorization": "Bearer ${GITHUB_TOKEN}" }
},
{ "name": "Public blog", "url": "https://acme.example/blog" }
]
}Three rules keep this safe:
- Mesh files never hold a literal token. Header values may reference
${ENV_VAR}; every loader that reads mesh JSON from local config (the CLI, the API's named meshes, the MCP catalog) resolves those references at load time and throws if a referenced variable is unset or empty, naming the mesh, source, and header. A missing secret fails at startup, not as a confusing401later. - Headers stay on their origin. Credential headers (
authorization,proxy-authorization,cookie) are sent to the source URL and its same-origin redirects only. A redirect or a discovered feed link on another origin is fetched without them, so a token meant for one host never reaches a host the page chose. See Fetching. - Only local config can set headers. Meshes supplied by a remote caller (
POST /mesh,POST /construct, and the MCPfetch_mesh/fetch_constructinline inputs) are parsed withPublicMeshSchema, which dropsheaders. A caller cannot make a server send credentials, and the${ENV_VAR}substitution never runs on their input, so they cannot read the server's environment through a URL they control.
Headers are not part of the conditional-cache key and are never written to the partial-failure warning on stderr.
Named meshes from config
Meshes can be stored as JSON files and referred to by name. The config directories are searched in order:
- Directories in the
NEUROWIRE_MESHESenv var (:or,separated). ~/.config/neurowire/meshes/(or$XDG_CONFIG_HOME/neurowire/meshes).
Files are matched as <name>.mesh.json then <name>.json. Names must be simple identifiers; anything path-like is rejected to prevent directory traversal.
Bundled ai-news
So that ?src=ai-news works with no setup, the API ships one built-in mesh, ai-news ("AI News"), with three sources: Claude Code Releases, the Claude Blog, and Simon Willison's feed (see packages/api/src/meshes.ts). User mesh directories take precedence over the bundled defaults.
TIP
A mesh groups sources into one flat feed. To group several meshes into a repo of feeds (with the per-mesh grouping preserved), use a construct.