Skip to content

moshpit: publish the allocation log so it can be mirrored - #406

Merged
ralyodio merged 1 commit into
mainfrom
worktree-moshpit-log-endpoint
Aug 15, 2026
Merged

moshpit: publish the allocation log so it can be mirrored#406
ralyodio merged 1 commit into
mainfrom
worktree-moshpit-log-endpoint

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

moshpit_tlds is a cache and moshpit_tld_log is the record — that distinction is written at the top of apps/pwa/src/moshpit.mjs, along with the claim that "the directory can be mirrored and served by anyone". It was true of the schema and false of the product: tldLog() had no HTTP route and no caller outside a test, so there was no way to read the order in which names were allocated, and therefore no way for a second copy of the registry to exist.

This adds GET /api/moshpit/log?since=&limit=.

What it answers

?since= is a seq and it is exclusive — the last entry a mirror stored. That is the only question a mirror ever asks, and answering it needs the server to remember nothing about who is asking, which is what makes the log mirrorable by anyone rather than by whoever we set up as a replica.

GET /api/moshpit/log?since=41&limit=500
{
"total": 5871, "since": 41, "limit": 500, "next": 541,
"entries": [{ "seq": 42, "tld": "eggs", "action": "name:scrambled",
"owner": "9f2c…", "at": 1755… }]
}

next is a cursor, null when caught up — not an absent field, which a client would reasonably read as "start again".

Decisions worth arguing with

  • Unauthenticated. A log only the operator can read settles nothing. The point of publishing the order is that a claim can be checked against it by someone who does not trust us, and a reader who has to ask us for permission first is trusting us again.
  • The owner is a digest, not the user id. This is the same line /api/moshpit/tlds/:tld already draws when it answers "already registered" without saying by whom: ownership is public, the account behind it is not. The digest is derived only from the id, so two entries by one owner are still visibly one owner and every mirror computes the same value.
  • Ordered by seq, never by at. Two entries can share a millisecond. A reader that sorts on the clock puts them in a different order than the writer did — at which point two mirrors disagree about who claimed a name first, which is the one fact this table exists to settle.
  • tldLog() now takes { since, limit }. Previously tldLog(limit); the only callers were two assertions in moshpit-registry.test.mjs that pass no argument, so nothing external changes.

What this does not do

Nothing is signed and nothing is chained yet. A mirror can now replicate the log, but it still cannot prove the log it received is the log we wrote, and owner_key remains a dead column. This is the first of five steps, and deliberately the one with no cryptographic commitments in it — the reserved-name list and the digest scheme should be settled before anything starts signing them, because signatures are what make those choices permanent.

Tests

apps/pwa/test/moshpit-log.test.mjs, 7 tests, no credentials anywhere in the file — every call it makes is the call a stranger's mirror makes. The one that matters is "following next from zero replays the log exactly once": it walks the cursor to exhaustion and asserts the result drops nothing, repeats nothing, and agrees on order with the unpaged read.

Full apps/pwa suite: 480 pass, 0 fail, 0 skipped.

🤖 Generated with Claude Code

`moshpit_tlds` is a cache and `moshpit_tld_log` is the record. That was
decided when the table was written and then went nowhere, because nothing
could read it: no route, no caller, no way for a second copy of the
registry to exist. "The directory can be mirrored and served by anyone"
was true of the schema and false of the product.
GET /api/moshpit/log?since=&limit= answers the only question a mirror
has -- everything after the last seq it stored -- without the server
holding any state about who is asking. Unauthenticated, because a log
only the operator can read settles nothing: publishing the order is what
lets a claim be checked by someone who does not trust us.
The owning account comes back as a stable digest rather than the user
id, which is the line /api/moshpit/tlds/:tld already draws when it says
"already registered" without saying by whom. Two entries by one owner
are still visibly one owner, and every mirror computes the same value.
tldLog() takes {since, limit} and orders by seq, never by `at` -- two
entries can share a millisecond, and a reader that sorts on the clock
puts them in a different order than the writer did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

const workdir = mkdtempSync(path.join(tmpdir(), "moshcode-log-test-"));
process.env.DATABASE_URL = `file:${path.join(workdir, "test.db")}`;
process.env.SESSION_SECRET = "test-secret";
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

99 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 42 | LOW: 52

SeverityRuleLocation
HIGHjs-ssrf-outbound-requestapps/pwa/public/sw.js:45
HIGHtls-verification-disabledapps/pwa/src/lib/moshpit-gateway.mjs:299
HIGHsh-remote-script-executioninstall.sh:79
HIGHsh-remote-script-executioninstall.sh:83
HIGHtls-verification-disabledsrc/dns.mjs:741
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:61
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:75
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:101
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:288
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:292
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:337
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:522
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:698
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:700
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:759
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:805
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:875
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:978
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1086
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1222
MEDIUMjs-unescaped-html-sinkapps/pwa/src/routes/moshpit.mjs:1484
MEDIUMjs-timing-unsafe-mac-compareapps/pwa/src/routes/settings-sync.mjs:192
MEDIUMjs-dynamic-code-executionapps/pwa/test/apikey-mask.test.mjs:129
MEDIUMsql-template-interpolationapps/pwa/test/credits-webhook-event-match.test.mjs:111
MEDIUMsql-template-interpolationapps/pwa/test/credits-webhook-event-match.test.mjs:131
MEDIUMsql-template-interpolationapps/pwa/test/moshpit-terms.test.mjs:192
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:128
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:417
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:543
MEDIUMjs-uninitialized-buffersrc/pty.mjs:120
MEDIUMjs-timing-unsafe-mac-comparesrc/settings-sync.mjs:325
MEDIUMinsecure-temp-filetest/dns-disable-restore.test.mjs:93
MEDIUMinsecure-temp-filetest/dns-disable-restore.test.mjs:310
MEDIUMinsecure-temp-filetest/news.test.mjs:693
MEDIUMinsecure-temp-filetest/plugins.test.mjs:152
MEDIUMinsecure-temp-filetest/pty.test.mjs:28
MEDIUMinsecure-temp-filetest/pty.test.mjs:31
MEDIUMinsecure-temp-filetest/pty.test.mjs:40
MEDIUMinsecure-temp-filetest/pty.test.mjs:42
MEDIUMinsecure-temp-filetest/pty.test.mjs:47
MEDIUMinsecure-temp-filetest/pty.test.mjs:48
MEDIUMinsecure-temp-filetest/pty.test.mjs:49
MEDIUMinsecure-temp-filetest/tabs.test.mjs:8
MEDIUMinsecure-temp-filetest/tabs.test.mjs:13
MEDIUMinsecure-temp-filetest/tabs.test.mjs:14
MEDIUMinsecure-temp-filetest/tabs.test.mjs:22
MEDIUMinsecure-temp-filetest/trust.test.mjs:240
LOWsecret-generic-credentialapps/pwa/test/apikey-bearer-scheme.test.mjs:30
LOWsecret-generic-credentialapps/pwa/test/apikey-mask.test.mjs:38
LOWsecret-generic-credentialapps/pwa/test/apikey-reveal.test.mjs:35

…and 49 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit b2e6419 into mainAug 15, 2026
4 checks passed
@ralyodioralyodio mentioned this pull request Aug 16, 2026
Sign up for freeto 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.

2 participants

@ralyodio@github-advanced-security