Skip to content

feat: the directory hosts webrings (OpenWebring), one ring per topic - #196

Merged
ralyodio merged 1 commit into
mainfrom
feat/openwebring
Sep 13, 2026
Merged

feat: the directory hosts webrings (OpenWebring), one ring per topic#196
ralyodio merged 1 commit into
mainfrom
feat/openwebring

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The directory hosts webrings: the first OpenWebring host

Spec: OpenWebring (profullstack/logicsrc#178). A ring is an ordered, circular list of member sites; every member carries its own word on who makes it (made_by: human, ai or both), read from the member's /.well-known/openwebring.json, never verified, absent shown as unstated. rssamplifier.com runs one ring per well-covered topic out of the feeds it already reads.

Schema

packages/db/migrations/20260913081529_openwebring.sql: rings and ring_members, purely additive. Ring order is an explicit position (a topic's feeds are re-derived on every crawl, so order cannot come from there); seeding is idempotent, new members append. The poller applies it at boot.

Routes

  • GET /.well-known/openwebring.json (a route, not a static file: rings are data)
  • GET /ring, GET /ring/[slug] (members in order, made_by, status, the three-anchor join snippet, <link rel="openwebring">)
  • GET /ring/[slug]/openwebring.json, GET /ring/[slug]/opml
  • Hops: GET /ring/[slug]/next|previous|prev|random and GET /ring/[slug]/[member]/next|previous|prev|random. ?from= canonical; ?host=, ?via=, ?url= and the bare Referer resolve too. 302, cache-control: no-store, no cookie, no write. Unknown from is a random member; wraps; skips members that are not active.
  • POST /ring/[slug]/check?url= (one member, behind the page gate)
  • GET /api/rings, GET /api/rings/[slug], PUT /api/rings/[slug]/members/[member] (owner of a claimed author profile, admin, or an OpenAccess bearer with openwebring:edit; added to openaccess.json)
  • MCP: list_rings, get_ring, ring_next (names added to openmcp.json)

Verification and seeding

packages/ingest/src/webring.js: checkRingMember is pure (takes its fetch), shared by the poller pass and the on-demand check. The poller (apps/poller/src/index.js, ringTick) seeds the top RING_TOPICS topics every RING_SEED_SECONDS and checks RING_BATCH members every RING_SECONDS, oldest verdict first, one fetch at a time; RING_VERIFY=0 pauses it. .env.example documents all of it. One-off: node --env-file=.env scripts/seed-rings.mjs.

Throttle

Hops are exempt from the proxy tiers (RING_HOP in proxy.js, read back by test/proxy.test.js): a refused hop is a broken link on somebody else's page. Still counted. Ring loads are cached 30 s per worker.

Shared code

linksBack and fetchPage in profileAuth.js now delegate to @rssamplifier/feed (linksTo, fetchPage) so the web app and the poller run one scan; OpenProfile callers are unchanged.

Verification

  • pnpm --filter @rssamplifier/web test: 460 pass, 0 fail (includes test/openwebring.test.js)
  • pnpm --filter @rssamplifier/db test, ingest, feed (344), poller (24): all pass (a first run of db and ingest failed with SQLite disk I/O errors while the box was swapping; clean on re-run)
  • pnpm --filter @rssamplifier/web build: compiles, every ring route listed

Built by a subagent from a repo survey; the agent died on the org's spend cap after finishing, so this PR was reviewed and verified by the parent session.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XYae2mH3khdwiXUVzcVMDw

rssamplifier.com is the first OpenWebring host (logicsrc.com/openwebring):
a ring is an ordered, circular list of member sites, and every member
carries its own word on who makes it, human, ai or both, read from the
member's /.well-known/openwebring.json and never verified.

- rings + ring_members (explicit position: a topic's feeds are re-derived
  every crawl, so ring order must be its own column; new members append).
- /.well-known/openwebring.json (a route: rings are data), /ring index,
  /ring/<slug> page with the join snippet, /ring/<slug>/openwebring.json,
  /ring/<slug>/opml, hops /next /previous /prev /random with ?from= and
  every shape older rings use (?host=, ?via=, ?url=, /<member>/next, the
  bare Referer); unknown from is random; wrap; skip inactive; 302 no-store.
- Verification in the poller (RING_* env): a bounded batch per tick, one
  stranger's front page at a time, any link to the ring counts, the member's
  descriptor read for made_by and rings[]; inactive, never removed. An
  on-demand /ring/<slug>/check behind the page gate.
- Hops exempt from the proxy throttle (a refused hop is a broken link on
  someone else's site), counted like everything else.
- Owner override PUT /api/rings/<slug>/members/<member>; OpenAccess scope
  openwebring:edit; /api/rings, /api/rings/<slug>; MCP list_rings,
  get_ring, ring_next; nav, sitemap, seed script.
- linksBack/fetchPage moved to @rssamplifier/feed (linksTo, fetchPage) so
  the poller and the web app run one scan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYae2mH3khdwiXUVzcVMDw
@ralyodio
ralyodio marked this pull request as ready for review September 13, 2026 08:50
@ralyodio
ralyodio merged commit accc9de into main Sep 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant