sirius-press/docs/publishing.md
Silent Mode 5465b65756 feat(sirius-press): a WordPress where the account is a key, not a mailbox
WordPress makes two assumptions this project cannot accept: that identity
comes from an email address, and that a site lives at one server. Both are
things somebody else can take away — a mailbox is rented from a provider who
can close it or be compelled to open it, and a server is one seizure from
being gone.

Sirius Press replaces the first and hedges the second.

Signing in means signing a challenge with the key that controls a CashAddress.
The address is recovered from the signature, so nothing is typed but the
signature itself, and the result is an ordinary WordPress session cookie —
roles, capabilities, nonces and the REST API never learn the login was
different. Three ways to produce one: a wallet the browser already exposes, a
phrase used once in the page and wiped, or a signature pasted in from any
BIP-137 wallet, which needs no JavaScript and lets the key stay on a machine
that never touches the web.

There is no password reset, and the recovery page says so plainly rather than
offering a form that cannot work. A reset mechanism is by construction a way
to take an account from its owner, and it is always easier to attack than the
cryptography it bypasses.

Publishing a post also exports it as static HTML to the name's storage on Sia,
signed by the key that owns the name, so the site keeps answering when the
server does not.

Email as a feature is untouched. wp_mail() still works, SMTP still sends, and
contact forms still deliver to addresses real people typed. Only mail to the
site's own unroutable placeholder addresses is diverted to an in-app inbox.
The objection was to email as identity, not to email.

Core is pinned and patched rather than vendored. WordPress 7.1.1 is 149 MB and
5,008 files; the fork's entire core diff is 75 lines in wp-admin/install.php.
Carrying the former to express the latter would bury the patch where nobody
reviews it and make every clone of the monorepo pay for it. Upstream releases
still merge through tools/update-wordpress.sh, which reapplies the series and
says exactly which hunk needs a human.

The cryptography is implemented twice — PHP on the server, JavaScript in the
page — because the server must verify and the browser must sign. Both are
pinned against libauth, the library the Sirius portal wallet and the BNS
gateway already use, so a disagreement of one byte fails the test suite rather
than presenting as a rejected login at three in the morning.

132 checks, no framework, about a second.
2026-09-21 01:39:38 +02:00

185 lines
7.1 KiB
Markdown

# Publishing to your name
Every page Sirius Press serves can also exist as a static file in your name's
storage, signed by the key that owns the name. This is what makes the site
outlive its server.
---
## What gets published, and when
Publishing or editing a post queues the pages it affects:
- the post itself
- the home page
- the blog page, if you have one
- every category, tag or other public archive it belongs to
- any asset those pages reference — stylesheets, scripts, images — the first
time it is seen
Unpublishing or deleting a post removes its file and re-queues the pages that
linked to it. A static mirror that keeps serving a post the author deleted is
worse than no mirror at all, because the author believes it is gone.
**Export everything** on the Publishing screen queues the whole site: every
published post and page, every public archive, the home page.
Password-protected posts are never exported. To the exporter they render as a
password form, and publishing that form to permanent public storage would be a
strange thing to do on purpose.
---
## Who signs, and where the key lives
The BNS gateway only accepts a write signed by the name's **current on-chain
owner**. That is the whole security model, and it has a consequence there is no
way around: whatever signs your uploads holds the key that owns your name and
whatever funds sit with it. There is no weaker credential to give a server.
So the question is not *how much* of your key the server gets. It is whether it
gets it at all.
### Manual — the default
Nothing is stored. Exports queue up; when you open **Sirius Press → Publishing**
you type your recovery phrase, and the page signs each file and uploads it
straight from your browser to the gateway. The phrase is cleared from the form
as soon as it has been used and the derived key is wiped when the run ends.
- Nothing sensitive on the server. A compromised host leaks your posts, which
were public anyway.
- A post published on a schedule sits in the queue until somebody signs in.
### Automatic
Your recovery phrase is encrypted with AES-256-GCM under a key derived from
`SIRIUS_PRESS_KEY` in `wp-config.php`, and stored in the database. Cron drains
the queue by itself.
- Scheduled posts publish while you sleep.
- Anyone who can read **both** your database and `wp-config.php` has your name
and your coins. That is most people who get a shell on the box.
The encryption protects one specific thing — a stolen database dump, a leaked
backup — and the settings screen says exactly that rather than implying more.
**A reasonable middle path:** run automatic mode with a wallet that holds the
name and nothing else. Keep your coins somewhere that has never been typed into
a web server. This does not protect the name, but it bounds what a compromise
costs to "the attacker can publish to my name until I move it", which is
recoverable.
---
## Derivation paths
Sirius Press derives its publishing key at `m/44'/145'/0'/0/0` — BCH coin type,
first account, first address. Same as the Sirius portal wallet, same as most
BCH wallets.
If your name was bought with a wallet minted somewhere else, the path may
differ. The symptom is unmistakable once you know it: every upload returns 403,
and **Sirius Press → Settings** shows two different addresses side by side —
the one your phrase derives, and the one the chain says owns the name.
Change the path in Settings. Operator wallets created from the Silent Mode CLI
were derived on the BTC coin type, `m/44'/0'/0'/0/0`.
The settings screen checks this for you as soon as you save a phrase: it
derives the address, asks the gateway who owns the name, and tells you whether
they match. A mismatch caught there is a sentence of explanation. The same
mismatch caught later is a 403 in a cron log nobody reads.
---
## The queue
Rows are keyed by path, not by post — several posts changing in a minute should
not queue the home page five times.
Each attempt renders the page, hashes the result, and compares it to what was
last published at that path. Identical bytes are marked done without an upload,
which is the common case: a WordPress page re-renders byte-for-byte far more
often than it changes.
Failures retry up to five times and then stop, and the first exhausted retry
puts a message in your inbox — once an hour at most, because a misconfigured
key fails on every row and four hundred identical messages are the same as
none.
### From the command line
```bash
wp sirius status # name, key, owner, queue depth
wp sirius export --all # queue everything, then push it
wp sirius export --limit=50 # work through part of the queue
wp sirius export --all --dry-run # build every page, upload nothing
```
`--dry-run` is the fastest way to find a page that renders badly for the
exporter without publishing anything.
The first full export of an existing site is the one job genuinely better in a
terminal: thousands of pages, a long run, and no browser tab that must stay
open. Note that CLI publishing needs automatic mode, since there is no browser
to sign in manual mode.
---
## What the exported pages look like
Pages are fetched over a loopback HTTP request, not rendered in-process. That
costs a round trip and is worth it: a theme's output depends on the whole
request lifecycle, and reconstructing it by calling `get_the_content()` gives
you something that resembles the page rather than the page.
Links are rewritten to be **document-relative**`../../about/index.html`
rather than `/about/index.html` or an absolute URL. Absolute links would drag
readers back to the origin server the export exists to make optional.
Root-relative ones break when the same bucket is browsed under
`/bns/<name>/` on the public gateway. Document-relative works in both places.
Asset URLs lose their `?ver=` query, because the bucket stores one copy of each
file and the query would only ever produce a 404.
### Things that cannot work in a static copy
Anything needing PHP at read time: the search form, the comment form, a cart, a
"related posts" widget that queries at render. They will appear and do nothing.
If your theme has such a thing, hide it during export:
```php
add_action( 'sirius_press_rendering_export', function () {
add_filter( 'get_search_form', '__return_empty_string' );
remove_action( 'wp_footer', 'my_theme_live_widget' );
} );
```
That action fires on the live request, while the exporter is fetching — which
is the right moment, because removing something afterwards means also unpicking
the styles and spacing it left behind.
---
## Hooks
```php
// Skip a post entirely.
add_filter( 'sirius_press_should_export_post', function ( $export, $post ) {
return 'private-type' === $post->post_type ? false : $export;
}, 10, 2 );
// Add to, or trim, what a full export covers.
add_filter( 'sirius_press_export_targets', function ( $targets ) {
$targets['feed/index.xml'] = array( 'url' => home_url( '/feed/' ), 'kind' => 'feed' );
return $targets;
} );
// Change which paths a post edit invalidates.
add_filter( 'sirius_press_affected_paths', function ( $paths, $post ) { }, 10, 2 );
// React to a successful upload.
add_action( 'sirius_press_exported', function ( $path, $bytes ) { }, 10, 2 );
```