Skip to content

feat(resolve): choose whether Moshpit overrides clearnet for a contested name - #48

Merged
ralyodio merged 1 commit into
mainfrom
feat/moshpit-resolution
Jul 31, 2026
Merged

feat(resolve): choose whether Moshpit overrides clearnet for a contested name#48
ralyodio merged 1 commit into
mainfrom
feat/moshpit-resolution

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The problem

Two namespaces now answer to the same shape of name. profullstack.ai is a real clearnet domain someone can squat — and it's also a name the Moshpit registry can hold. Something has to decide which one a navigation means, and it can't be hardcoded:

  • a user who has never heard of Moshpit must keep getting clearnet
  • an operator who registered the name expects their version to win

The setting

Mode Behaviour
clearnet (default) Clearnet owns any name clearnet can answer. Moshpit is consulted only where DNS came up empty — the registry acts as a backfill, filling gaps rather than shadowing the existing web.
moshpit A registered name wins even when clearnet has an answer. This is the override: the point of registering profullstack.ai in Moshpit is that your version is the one you get.

clearnet is the default because silently redirecting a domain that resolves perfectly well is indistinguishable from hijacking it. Opting into the override is a decision the user makes, not one we make for them.

Names under an ending clearnet has never heard of — .yeah, .sploof, .oranges — resolve through Moshpit in either mode. Nothing conflicts, and refusing them would defeat the whole namespace.

Design

decideResolution is pure and total: no network, no browser, every branch returns a decision with a reason. The caller never has to invent behaviour for an unhandled combination, and because the reason is surfaced, an override never looks like a glitch.

A registry outage must not take the ordinary web down with it. Resolution sits in front of every navigation, so lookupMoshpit returns null — never throws — on a timeout, a non-2xx, a connection failure, or a nonsense payload, and every one of those falls back to clearnet.

parseRegistryName refuses anything that isn't exactly one label and one TLD, so a.b.c, localhost, 192.168.1.1 and host:port are never sent to the registry as if they were names. Verified by test that the fetch isn't even attempted.

Extension UI

New Name resolution section in the extension options: the mode picker plus an advanced registry field, so a self-hosted pit can be pointed at instead of pit.moshcode.sh. Absent config reads as the default rather than as "off".

Tests

16 new cases in apps/desktop/src/moshpit-resolve.test.ts, including the squatting scenario from both sides, alias following (profullstack.agenticprofullstack.agent), outage fallback in both modes, and an exhaustive sweep asserting every mode × DNS × registry combination produces a non-empty reason.

Full desktop suite: 53 passed.

Scope

This lands the resolution policy, the registry client and the setting. Wiring it into live navigation interception is not included — that needs a webNavigation permission bump in the MV3 manifest and a decision about the gateway path (gatewayUrlFor currently assumes /n/<name>, which PRD 0002 R7 leaves open). Worth settling that before intercepting real traffic.

🤖 Generated with Claude Code

…ted name

Two namespaces now answer to the same shape of name. `profullstack.ai` is
a real clearnet domain someone can squat, and it is also a name the
Moshpit registry can hold. Something has to decide which one a navigation
means, and that cannot be hardcoded: a user who has never heard of
Moshpit must keep getting clearnet, while an operator who registered the
name expects their version to win.

So it is a setting, with two honest positions:

  clearnet (default) — clearnet owns any name clearnet can answer;
      Moshpit is consulted only where DNS came up empty, making the
      registry a backfill that fills gaps rather than shadowing the
      existing web. Default because silently redirecting a domain that
      resolves perfectly well is indistinguishable from hijacking it.

  moshpit — a registered name wins even when clearnet has an answer.
      This is the override: the point of registering profullstack.ai in
      Moshpit is that your version is the one you get.

Names under an ending clearnet has never heard of (.yeah, .sploof)
resolve through Moshpit in either mode — nothing conflicts, and refusing
them would defeat the namespace.

decideResolution is pure and total: every branch returns a decision with
a reason, so the caller never invents behaviour for an unhandled
combination and an override never looks like a glitch. A registry that is
down, slow, or serving nonsense yields null and falls back to clearnet —
resolution sits in front of every navigation, so a registry outage must
not take the ordinary web down with it.

parseRegistryName refuses anything that is not exactly one label and one
TLD, so `a.b.c`, `localhost` and IP literals are never sent to the
registry as if they were names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 5bad141 into main Jul 31, 2026
6 of 7 checks passed
ralyodio added a commit that referenced this pull request Jul 31, 2026
…ured parts

CI has been red on main since #48: noUncheckedIndexedAccess types the
destructured `label`/`tld` as `string | undefined`, because TS can't narrow an
array to a 2-tuple from a `parts.length !== 2` check. Four TS2345/TS2322
errors failed `apps/desktop` and took the whole `pnpm -r build` with it.

Guard explicitly. Behavior is unchanged: length 2 already guarantees both
values, and an empty label would fail LABEL.test() on the next line anyway.

Unrelated to this branch's auth work, but it blocks the PR from going green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio added a commit that referenced this pull request Jul 31, 2026
* fix(auth): extension CoinPay sign-in never picked up the session

Clicking "Sign in with CoinPay" in the extension's options page signed the
WEBSITE in and left the extension signed out.

coinpay-auth.js asked the backend to redirect back to
chrome.identity.getRedirectURL() -> https://<ext-id>.chromiumapp.org/.
That target is off-origin, so safeRedirect() rejected it (correctly — the
session token rides in the redirect fragment) and /api/auth/coinpay/callback
fell through to `c.redirect(`${APP_URL}/?signedin=1`)`. The OAuth dance
completed, tronbrowser.dev got its tb_session cookie, and the extension got
nothing. (Ungoogled Chromium also rewrites chromiumapp.org to a
non-resolving .qjz9zk host, so launchWebAuthFlow could not have worked
either.) bittorrented.js already worked around this with ext-callback;
CoinPay never got the same treatment.

- Add GET /api/auth/ext-login: validates an on-origin callback, and when the
  user is ALREADY signed in on the website hands a session straight back
  instead of running CoinPay a second time. Otherwise forwards to the
  CoinPay dance preserving the callback.
- coinpaySignIn() opens that URL in a tab and waits for the token, matching
  the bittorrented flow. Listener is registered before the tab is created so
  a fast redirect can't race it.
- ext-callback.js now serves both flows, keyed on `?src=tb`, and background
  handles the generalized 'ext-token' message.
- Drop the now-unused "identity" permission from the manifest.

Redirect targets stay same-origin only, so a drive-by navigation to
/api/auth/ext-login lands the token in a fragment the navigating site
cannot read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(resolve): unbreak the build — narrow parseRegistryName's destructured parts

CI has been red on main since #48: noUncheckedIndexedAccess types the
destructured `label`/`tld` as `string | undefined`, because TS can't narrow an
array to a 2-tuple from a `parts.length !== 2` check. Four TS2345/TS2322
errors failed `apps/desktop` and took the whole `pnpm -r build` with it.

Guard explicitly. Behavior is unchanged: length 2 already guarantees both
values, and an empty label would fail LABEL.test() on the next line anyway.

Unrelated to this branch's auth work, but it blocks the PR from going green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio ralyodio mentioned this pull request Jul 31, 2026
@ralyodio
ralyodio deleted the feat/moshpit-resolution branch August 4, 2026 04:29
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