Skip to content

@neurowire/api ​

The Neurowire HTTP service (version 0.5.0): a Hono app that serves feeds, meshes, and constructs as NWF, Atom, RSS, JSON Feed, or Markdown, and streams them live over SSE. It registers the built-in taps at startup and caches both the serialized response and the upstream fetches.

bash
npm install @neurowire/api

Depends on core, ingest, taps, and hono.

Library exports ​

ts
import { app } from '@neurowire/api'

The package exports the Hono app (from both index.ts and app.ts). index.ts also runs the standalone server (@hono/node-server) when executed directly, listening on PORT (default 8787).

ExportTypeDescription
appHonoThe configured Hono application. Mount it, run it with any Hono adapter, or call app.fetch(request) directly (e.g. in tests).

Running it

pnpm api starts the server. The bundled server entry calls serve({ fetch: app.fetch, port }) and logs the listening URL.

Endpoints ​

All feed-shaped responses set Content-Type from the format's media type and Cache-Control: public, max-age=300. The format query defaults to atom and must be one of nwf, atom, rss, json, md (an unknown value returns 400). HTML is not a feed format, so format=html is rejected like any unknown format.

How a request moves through the serviceFeed routes resolve a target, check the TTL response cache, fetch upstream through a conditional cache, then serialize. The tail route holds long-lived streams on shared poll loops, and the sync routes read the journal store from disk without touching the network.RequestHono app/feed /mesh /constructserialized, cached 300s/tailone stream, held open/sync/*read only, opt-in publishmisspollUpstream fetchconditional cache, 304 on repeatShared poll loopby target, interval, journalJournal store on diskNWFJ segments, no networkappends, when journaled
Three shapes of route on one app: cached one-shot serializations, long-lived streams sharing an upstream poll, and read-only reads straight off the journal store.

GET / ​

Service descriptor. Returns JSON with name, version, the supported formats, the endpoints summary, and the available meshes and constructs names.

GET /healthz ​

Liveness probe. Returns { status: 'ok', service: 'neurowire', version: '0.5.0' }.

GET /feed ​

Fetch a single URL and serialize it.

QueryRequiredDefaultDescription
urlyes-The website or feed URL (URL-encoded).
formatnoatomOutput format.

Responses: 200 with the serialized feed; 400 when url is missing or format is unknown; 502 ({ error, detail }) when the upstream fetch or build fails.

GET /mesh ​

Serialize a named mesh (resolved via mesh resolution).

QueryRequiredDefaultDescription
srcyes-The mesh name.
formatnoatomOutput format.

Responses: 200; 400 (missing src or unknown format, body lists available meshes); 404 (unknown mesh, body lists meshes); 502 on build failure.

POST /mesh ​

Serialize an inline mesh from the request body.

  • Body: a JSON Mesh (validated with PublicMeshSchema: a headers key on a source is dropped, so a request cannot attach credentials).
  • Query: format (default atom).
  • Responses: 200; 400 (unknown format or invalid mesh body, with detail); 502 on build failure.

GET /construct ​

Serialize a named construct (resolved via construct resolution), flattened into one feed.

QueryRequiredDefaultDescription
srcyes-The construct name.
formatnoatomOutput format.

The construct is fetched with the upstream cache and resolveMesh as its { ref } resolver, then flattened. Responses: 200; 400 (missing src or unknown format, body lists constructs); 404 (unknown construct, body lists constructs); 502 on build failure.

POST /construct ​

Serialize an inline construct from the request body.

  • Body: a JSON Construct (validated with PublicConstructSchema: inline meshes lose any per-source headers).
  • Query: format (default atom).
  • Responses: 200; 400 (unknown format or invalid construct body, with detail); 502 on build failure.

Construct format note

The API serves only flattened feed formats for constructs. The grouped, multi-page HTML view lives in @neurowire/web.

GET /tail ​

Follow a target as a server-sent event stream, built on Hono's streamSSE.

QueryRequiredDefaultDescription
url / src / constructone of-The target, same values as /feed, /mesh, and /construct.
formatnojsonjson (one entry object per event) or nwf (that entry's NWFJ lines).
intervalno300Seconds, or a duration like 15m. Clamped up to a 60 second floor.
journalno-A journal id already present in the store, which enables cursor resume.
sinceno-A journal cursor to replay from; Last-Event-ID carries the same value.

Events: init (target, format, effective interval, resume of journal or live, journal head, replayed count), entry per entry with its cursor as the event id, and a : ping comment every NEUROWIRE_TAIL_HEARTBEAT_MS. Responses: 200 (text/event-stream, plus X-Accel-Buffering: no); 400 (no target, or an unknown tail format); 404 (unknown mesh or construct). Every error is decided before the stream opens.

packages/api/src/tail.ts keeps a registry of poll loops keyed by target, interval, and journal id, so all clients asking for the same thing share a single upstream poll; the loop starts with the first subscriber and is torn down when the last one leaves. A late joiner is handed the newest items the loop has already broadcast (up to 50) so it is not behind. With a journal attached the loop appends what it sees and seeds its seen-set from the journal, which is what makes event ids real cursors and Last-Event-ID a lossless resume.

ExportDescription
tailHandler(c)The route handler mounted at GET /tail.
resolveTailTarget(query)Resolve url/src/construct into a loadable target, or a 400/404 body.
resolveTailInterval(raw)Apply the default and the 60 second floor to a requested interval.
resolveTailJournal(id)The journal to replay from and write through, when the id exists.
parseTailCursor(value)Parse 42 or 42.<hash> into a JournalCursor.
subscribeTail(key, options, listener)Attach to (or start) the shared poll loop for a target.
tailLoopCount() / stopAllTails()Inspect and tear down the running loops.
heartbeatMs()The keep-alive comment interval.
TailTarget, TailItem, TailBroadcast, TailJournal, TailLoopOptions, TailSubscriptionThe registry's types.

See the Tail concept page for the polling semantics this route inherits from pollFeed.

Sync endpoints ​

packages/api/src/sync.ts serves nwf-sync/1, the peer delta-exchange protocol: four read-only routes that let another node pull journal deltas instead of re-fetching every upstream source itself. They are mounted at /sync/*, and every response carries NWF-Sync-Version: 1.

Nothing is published by default. A node exposes journals explicitly, via NEUROWIRE_SYNC_PUBLISH or ~/.config/neurowire/sync.json. An unpublished id and a nonexistent id answer the same 404.

EndpointReturns
GET /sync/journalsJSON: the published journals, each with id, title, head, entries, segments, bytes, updated.
GET /sync/head?journal=<id>JSON { journal, head, hash? }. The cheap poll target.
GET /sync/since?journal=<id>&cursor=<c>One NWFJ segment after c (application/x-nwf-journal); 204 when up to date, 410 when the cursor predates retention.
GET /sync/snapshot?journal=<id>&cursor=<c>The same, except a too-old cursor is clamped instead of failing. Bootstrap and 410 recovery.

A 200 from since or snapshot adds NWF-Sync-Journal, NWF-Sync-Head, NWF-Sync-Range (<firstSeq>-<lastSeq>), NWF-Sync-Segment, and NWF-Sync-Complete (1 when the response reaches the head). Responses hand back one whole segment, never a concatenation: NWFJ dictionary indices are per segment and the chain reseeds at each J header, so glued segments would decode and verify wrongly.

Statuses: 200; 204 (cursor at or past the head); 400 (missing journal, path-like id, or an unparseable cursor); 401 (a token is configured and was not presented); 404 (not published); 410 (since only, body points at /sync/snapshot).

Module exports ​

packages/api/src/sync.ts exports the sub-app and its helpers for anyone assembling a custom server around them. The published package's entrypoint exports only app.

ExportDescription
syncThe Hono sub-app holding the four routes, mounted by app.ts at /sync.
SYNC_VERSION / SYNC_VERSION_HEADER1 and NWF-Sync-Version.
loadSyncConfig()The publish list and token: sync.json first, environment on top. A corrupt file publishes nothing rather than crashing.
publishedJournalIds(config, store)The ids this node serves: every explicitly named id, plus every journal on disk when the list contains *.
isPublished(id, config, store)Whether one id is exposed.
describeJournal(store, id)One journal's listing entry. Everything but the title comes from the manifest.
headCursor(store, id)The head cursor including its chain hash, read from the tail of the newest segment (the store's own head() reports only the sequence number).
parseSyncCursor(raw)Parse a cursor query value (42 or 42.<hash>); undefined when unparseable.

The chain is a checksum, not a signature

The bearer token gates access and the hash chain detects corruption. Nothing here authenticates content origin, and nwf-sync/1 does not sign anything. You sync from peers you chose to trust, over TLS your proxy terminates. See the trust model and the federation guide.

Mesh resolution ​

packages/api/src/meshes.ts resolves a mesh name to a Mesh. User mesh directories are searched first (NEUROWIRE_MESHES, then ~/.config/neurowire/meshes), then the bundled defaults.

ts
function resolveMesh(name: string): Mesh | undefined
function listMeshNames(): string[]
ExportDescription
resolveMesh(name)Resolve a named mesh (tries <name>.mesh.json then <name>.json in each dir, then the bundled ai-news). Path-like names are rejected.
listMeshNames()Sorted names of all available meshes (bundled plus any found in the directories).

A built-in ai-news mesh ships so ?src=ai-news works with no setup.

Construct resolution ​

packages/api/src/constructs.ts mirrors mesh resolution for Constructs, searching NEUROWIRE_CONSTRUCTS then ~/.config/neurowire/constructs, then the bundled defaults.

ts
function resolveConstruct(name: string): Construct | undefined
function listConstructNames(): string[]
ExportDescription
resolveConstruct(name)Resolve a named construct (tries <name>.construct.json then <name>.json, then the bundled daily). Path-like names are rejected.
listConstructNames()Sorted names of all available constructs (bundled plus directory entries).

A built-in daily construct ships so ?src=daily works with no setup.

Response cache ​

packages/api/src/cache.ts provides the tiny in-memory TTL cache the handlers use for the serialized result. The handlers also keep a createMemoryCache conditional cache so upstream fetches can 304 on a TTL miss.

ts
interface CacheEntry {
  body: string
  contentType: string
  expires: number
}

interface TtlCache {
  get(key: string, now: number): CacheEntry | undefined
  set(key: string, entry: CacheEntry): void
}

function createTtlCache(): TtlCache
ExportDescription
CacheEntryA cached body, its contentType, and an expires epoch-ms timestamp.
TtlCacheA TTL cache; now is injected into get so expiry is testable.
createTtlCache()Create a Map-backed TtlCache.

Environment variables ​

VariableDefaultDescription
PORT8787Port the standalone server listens on.
NEUROWIRE_CACHE_TTL300Response cache TTL in seconds (matches the Cache-Control: max-age=300).
NEUROWIRE_MESHES-:/,-separated directories searched for named meshes.
NEUROWIRE_JOURNAL-Journal directory GET /tail replays from (else ~/.config/neurowire/journal).
NEUROWIRE_TAIL_HEARTBEAT_MS25000How often GET /tail writes its keep-alive comment.
NEUROWIRE_CONSTRUCTS-:/,-separated directories searched for named constructs.
NEUROWIRE_TAPS-Extra taps loaded at startup via registerAllTaps().
NEUROWIRE_JOURNAL~/.config/neurowire/journalJournal store directory the /sync/* routes read.
NEUROWIRE_SYNC_PUBLISH-:/,-separated journal ids to publish over /sync/*, or * for all. Nothing is published without it.
NEUROWIRE_SYNC_TOKEN-Static bearer token required on every /sync/* request. Unset means open.
NEUROWIRE_SYNC_CONFIG~/.config/neurowire/sync.jsonPath to the { publish, token } config file.
XDG_CONFIG_HOME~/.configBase for the default mesh/construct/tap directories.