Commit graph

106 commits

Author SHA1 Message Date
Local Dev
8979ec11cc feat(gateway): accept *.bns.navigate.st alongside *.bch.navigate.st
Preparing to rename the subdomain namespace from .bch to .bns. ".bch"
reads as "Bitcoin Cash TLD" but is applied uniformly to names on every
BCDN TLD — sirius-x.bch.navigate.st reads wrong when sirius.x is a .x
name. ".bns" (Bitcoin Cash Name Service — the real protocol name) is
the same length, carries meaning, and doesn't lie about the name's TLD.

Transition approach (no cutover required):
  * BNS_SUB_BASE keeps defaulting to "bch.navigate.st" — primary suffix
    for new URLs (banner "Isolated view", OG hints) stays unchanged
    until the .bns wildcard cert is issued.
  * BNS_SUB_BASE_LEGACY, defaulting to "bch.navigate.st,bns.navigate.
    st", lists every suffix the inbound matcher accepts. Both forms
    resolve to the same name via the existing subdomainLabelToName.
  * Bare apex of ANY accepted suffix serves the gateway home.

Verified: sirius-x.bch.navigate.st still 200s, and curl -H
'Host: sirius-x.bns.navigate.st' http://127.0.0.1:8700/ returns the
same Sirius.X HTML (gateway handler accepts both; the public TLS
termination for .bns.navigate.st is pending the wildcard cert).

Rollout:
  1. Issue *.bns.navigate.st wildcard (DNS-01, manual TXT on nymdns)
  2. ln -s /etc/nginx/sites-available/bns-subdomain (staged)
  3. systemctl reload nginx
  4. Flip BNS_SUB_BASE=bns.navigate.st so new banner links use .bns.
  5. Keep .bch. live for ~6 months as the deprecation window closes.
2026-10-07 19:12:48 +02:00
Local Dev
eb20a5b778 fix(theseus,gateway): don't leak relay chrome into BNS-aware clients
Diagnosis: Theseus does not actually redirect to navigate.st — but for
`s3`-record names, it fetches content THROUGH /bns/<name>/ on the
gateway (Sia keys cannot ship in a public build). Since 0.3.80 that
fetch carries `x-bns-policy: client` so the gateway suppresses the
relay banner + chip; older Theseus releases got the chrome bleed-
through, which read as "redirected to navigate.st" even though the
address bar still showed https://<name>.

Two-layer defense:

Gateway:
  Each injected block is now wrapped in `<!--nav-relay-X-start-->` /
  `<!--nav-relay-X-end-->` sentinel comments so any downstream client
  can strip it surgically without parsing the nested markup. Function
  is unchanged for browsers (chrome shown by default; suppressed on
  x-bns-policy: client).

Theseus main.js serveBns() s3 branch:
  Already stripped the injected `<base href="/bns/...">` tag. Now also
  strips any relay banner / chip / boot script found between the
  sentinel comments. Catches older gateways, misconfigured deploys,
  or any future edge cache that returns a chrome-bearing response.
  No change to h / ip / p / u paths — those don't touch the gateway.

Other BNS-aware clients (Ariadne daemon, Ariadne mobile) that also
proxy s3 through the gateway should apply the same sentinel-based
stripping or send x-bns-policy: client. The gateway honours either.
2026-10-06 22:34:25 +02:00
Local Dev
32d514ecc4 gateway: no relay banner inside Theseus and Ariadne
They fetch pages through the path form and show them under the name's own
origin, so the banner warned about a gateway origin the page was not on.
2026-10-06 22:05:50 +02:00
Local Dev
53fd569b0d feat(gateway): Tier 2 relay hardening — countdown, chip, ACAO lockdown
Three additions on top of last commit's security headers + banner.

Countdown auto-redirect (path form banner):
  Visible "Moving in <N> s" between the message and the Isolated-view
  button, default 10 s, counts down once per second and location.replace
  to the subdomain form on expiry. The whole thing stops instantly on:
    - any click / keydown / wheel / touchstart / scroll / pointerdown
      (we don't yank a page out from under someone who's reading it),
    - "Stay here" button (persists for the tab via sessionStorage),
    - "✕" dismiss (persists for 24 h via localStorage dismissed_at).
  First-time visitors drift to the isolated origin; return visitors
  who've once said Stay are left alone for the rest of the session.

Persistent trust chip (both forms):
  Bottom-right pill reading "📡 <name> · via navigate.st", with
  backdrop blur so it reads over any page. Click opens a 280 px pop
  that explains the trust context: on the PATH form it adds an
  Isolated-view button; on the SUBDOMAIN form it reads
  "Each name has its own origin here. Scripts can't reach other BCDN
  sites." Essential on mobile where URL bars hide the subdomain
  structure — the user can still see which BCDN name they are on and
  that it is being relayed through navigate.st. Click-away collapses.

ACAO lockdown on HTML:
  res.removeHeader("access-control-allow-origin") before every HTML
  writeHead so relayed PAGES are no longer readable cross-origin.
  Static assets (fonts, images, JSON) keep the broad ACAO because
  embedding those from BCDN names on other origins is a legit use
  case; HTML being readable cross-origin was a surveillance surface
  (any site could fetch BCDN pages and inspect their rendered content)
  with no legit case the subdomain form doesn't already cover.
2026-10-06 07:02:07 +02:00
Local Dev
1ec4fdde7d feat(gateway): security hardening — headers, relay banner, subdomain fix
Option (a) from the security review: /bns/ path form stays live but
now carries a visible warning banner + a one-click upgrade to the
subdomain form where each BCDN name gets its own browser origin.
Security headers go on every relayed HTML response regardless of form.

New headers on every relayed HTML (both /bns/ and *.bch.navigate.st):
  content-security-policy: permissive on scripts/styles (real BCDN
    sites use inline), but frame-ancestors 'none', object-src 'none',
    base-uri 'self', form-action 'self' https: — closes clickjacking,
    <object>/<embed> smuggling, <base> poisoning, cross-origin form
    submits in one block.
  permissions-policy: camera, mic, geolocation, payment, usb,
    bluetooth, accelerometer, gyroscope, magnetometer, autoplay,
    interest-cohort, browsing-topics all denied by default. A
    malicious BCDN page can no longer ask the user for sensitive APIs
    with navigate.st-flavoured prompts.
  referrer-policy: no-referrer
  x-content-type-options: nosniff
  x-frame-options: SAMEORIGIN (belt-and-suspenders alongside CSP)

Relay banner (PATH FORM only):
  Fixed top strip reading "⚠ Relayed site · <name> is being served
  through navigate.st · Scripts here run under the gateway's origin —
  do not sign in or paste a recovery phrase". Offers an "Isolated
  view ↗" button to the subdomain form and a secondary "Install
  Theseus" link. All inline, no external deps, `all:initial` on every
  node so the host page's CSS cannot style it. Dismissible for 24h via
  localStorage. Body padding adjusts on load+resize so the banner
  doesn't overlap content.

Subdomain-form decoder fix:
  Old handler hard-appended ".bch" on reverse — only worked for
  2-level .bch names. sirius-x.bch.navigate.st decoded to the
  phantom "sirius.x.bch". New scheme is lossless for every BCDN name:
    hello-bch                -> hello.bch
    sirius-x                 -> sirius.x
    coinspectrum-deviant-bch -> coinspectrum.deviant.bch
    hello--world-bch         -> hello-world.bch (dashes in label survive)
  Legacy short form "hello.bch.navigate.st" still resolves — a
  dot-less single label is treated as an implicit ".bch" name for
  back-compat.
2026-10-06 01:25:48 +02:00
Local Dev
559eb6996a sirius-x: the dashboard knows covenant-held names, and can register with a commit first
Covenant-held names now list under their owner with an expiry badge, warn
from sixty days out, and get an Expiry tab: move an ordinary name into the
covenant (one payment, no way back - the page says so), or renew. Records,
transfer and release go through the covenant for such a name; a listing
cannot, and the Sell tab says why. A name just sent into the covenant is
remembered until its block, so it does not vanish from the list meanwhile.

Registering offers "Commit first": a hash goes out now and the name a block
later, so it cannot be copied out of the mempool. The pending commit is
stored before it is broadcast, the open tab reveals by itself once the
commit is mined, and the dashboard lists registrations in progress and can
finish one the tab did not live to complete.

Also: a one-cent name's platform share of the fee was below the dust limit,
which made the registration unmineable; a dust-sized share now goes with
the other one.
2026-10-06 00:40:00 +02:00
Local Dev
a9f4bfa95a covenant names: the actions a dashboard needs, and holdings that list them
A covenant-held name's certificate is at the covenant's address, so the
gateway's holdings - which looked for certificates among a wallet's coins -
never listed it under anyone. It is now listed under the key its verified
state names, with its expiry and where it is in its life.

covenant-registrar.js is one function per thing an owner does with the
built-in wallet: move a name into the covenant, renew, publish records,
transfer, release. Each announces the new state, and refuses a stale view
of the name rather than guess where the certificate went. Tested against a
simulated chain: every transaction is run through the VM, applied, and
read back through the resolver.

The name index fetched its transactions one at a time, which had grown to
most of half a minute before a registration could be quoted; it now
fetches eight at a time.
2026-10-05 23:51:02 +02:00
Local Dev
b1c77db313 docs: commit-reveal as built, and its chipnet run; site bundle rebuilt
The addendum gains a section stating the rule that was implemented and
why the contest window and the commit's lifetime differ from the first
specification. The run records a front-runner registering a name first
and losing it to the older commit.
2026-10-05 22:56:56 +02:00
Local Dev
cc683130b5 registrar: an older commit can still register a name inside its contest window
A front-runner's registration makes the name look taken, and the
registrar refused to reveal - exactly when revealing is what wins the
name back. The availability check now says whether the contest is open
and what a challenger must beat, and a reveal whose commit is older goes
ahead. Index entries record the commit a registration revealed, so that
comparison can be made.
2026-10-05 22:34:00 +02:00
Local Dev
d19cd6bde7 BNS: commit-reveal for first registrations
A registration shows its name to the mempool before it is mined, and
"first REG wins" gives the name to whoever copies it with a better fee.
A registrant can now publish a hash first (CMT) and reveal a block later:
the REG carries the salt, and counts only if a commit of
sha256(salt, name, certificate lock) is already 1..144 blocks deep.
Someone who learns the name from the reveal has no such commit.

Who wins a contested name: the first valid REG opens a window of six
blocks, and within it the REG with the oldest commit takes the name; a REG
without a commit ranks by its own block. After the window the registration
is final. The addendum first let an older commit win for as long as the
commit lived, which left every registration open for a week and let a
standing commit beat any buyer; and a commit now lives a day, so keeping
one alive costs more than owning the name.

Optional on chipnet, where plain registrations keep working exactly as
before (the live registry reduces to the same index). Required on mainnet,
which starts empty. A registration straight into the covenant commits to
the covenant address, fixing owner and expiry at commit time.

One implementation for the JavaScript reducers; the Android reducer is now
a pure function and agrees with it on every history the tests build.
2026-10-05 22:29:38 +02:00
Local Dev
03b52727bf Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.

- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
  right number of dots and submit on the last digit; a PIN of the wrong
  length is refused without a strike, so an older Aegis pad cannot burn the
  count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
  (6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
  which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
  offers to save (or update) the login, with an optional "ask for my PIN or
  password before filling it". Focusing a login form offers the saved
  logins under it; on a locked vault it offers to unlock first. A failed
  login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
  cookies and site storage of sites with a saved login. Their hostnames are
  kept sealed with the OS keystore so the list is readable at quit while
  the vault is locked. Verified end to end on a scratch profile: signed in,
  restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
Local Dev
18385600fa docs: how a resolver reads a covenant-held name; site bundle rebuilt
The addendum states the claim, the paid-entry rule, why a name inside the
covenant answers only to a verified state, and when a name stops
resolving. The registration bundle is rebuilt so the dashboard and market
index names by the same rules as the gateway.
2026-10-04 18:14:49 +02:00
Local Dev
9404c024b7 BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.

The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.

Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.

A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.

Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00
Local Dev
a2e5ebffb4 docs: what the covenant review found, what changed, and what is left open
Records the findings against each contract, the one-message rule in the
protocol, the chipnet run of the fixed migrate path, and which earlier
chipnet transactions used contracts that have since been superseded.
2026-10-04 16:53:59 +02:00
Local Dev
b4fc73f0fa BNS: a transaction with more than one protocol message carries none
Resolvers took the last message in a transaction. Wherever one party fixes
a message and another assembles the transaction - the delivery escrow pins
the registration at output 1 - a later output could replace it and every
resolver would read that one. Counting instead of choosing removes the
question: one message, or the transaction says nothing.

Same rule in all three resolvers. The Android parser now recognises the
same set of operations as the JavaScript one, since what counts as a
message decides which transactions are void. No transaction on chipnet
carried more than one.

Three older suites built registrations that never created their
certificate's category; their fixtures now do, as the sole-certificate
rule requires.
2026-10-04 16:53:58 +02:00
Local Dev
e0eca1190e covenants: close the holes an adversarial review found
migrate let the owner supply the new state, so an owner could put a script
of their own in front of the approved code and take the certificate out of
the covenant, or rewrite owner and expiry on the way. The contract now
carries its own state over and the owner supplies only the code. The
steward's approval covers source and destination code, so it cannot be
replayed in another tier or network.

The delivery escrow could be collected with a second protocol message after
the agreed one; it now allows at most four outputs, the fourth plain change.
A subscription draw could pay its fee allowance to a third output; it now
has one or two outputs and pays the host exactly the price.

Smaller: the commit salt is exactly 32 bytes (one box could serve several
names), expiry stays a block height, and relative-lock terms are range
checked in script. Escrow quotes are refused for co-sign TLDs, where a
delivery would satisfy the contract and register nothing.
2026-10-04 16:53:56 +02:00
Local Dev
4fb37e0683 bns: a registration is void unless its certificate is the only token of its category
Ownership of a name is proven by holding a token of its category: an update
is accepted when some output carries it. Nothing required that the
certificate be the only such token. Whoever registered a name could mint a
second one in the same transaction, a minting token for instance, sell the
certificate on the market, and later publish an update the resolvers would
accept. The buyer would hold the certificate and lose the name.

A registration now counts only if its category is created by that very
transaction, exactly one output carries it, and that output is an NFT with
no capability and no fungible amount. Nothing meeting this can ever have a
sibling. The same holds for TLD certificates, whose category proves the
right to set a TLD's policy and to co-sign registrations under it.

One definition, in resolver-web.js; bns.js and the TLD snapshot builder call
it. Every valid registration on chipnet already satisfied the rule: the live
registry is the same 71 names and 21 TLDs before and after.

Found while writing the delivery escrow, which had to forbid exactly this.
2026-10-04 16:13:04 +02:00
Local Dev
9aa8b6b33f covenants: the subscription and the delivery escrow, on one shared base (draft)
The roadmap asks for the covenant capability to be built once and used three
times. covenant.js is the shared part: artifact to bytecode, redeem script and
lock, signing a covenant input, an unlocking script for any function, and
assembling a transaction around covenant inputs. The registry library now
sits on it, and so do the two new contracts.

Subscription: the buyer funds a balance. Anyone may send one period's price
to the host once per period, the rest going back to the same script; the
buyer may take the balance back at any time with only their signature. A
draw spends its input alone, because with two inputs one payment and one
return could satisfy both and the host would keep a balance.

Delivery escrow: a payment that can be collected only by the transaction
that mints the agreed certificate to the buyer and publishes the agreed
registration, or refunded by the buyer after a wait. It forbids any other
token in the transaction: resolvers accept an update from any token of a
name's category, so a deliverer who minted a second one alongside the
certificate could later rewrite the buyer's name. It also sits at a fixed
input, so two payments cannot be collected against one certificate.

Neither can see the registry: an escrow cannot know the name was taken
between payment and delivery. The contract says so.

Drafts, not reviewed. The tests run every path in the VM under the 2025 and
2026 rules and count that the VM itself refused the tampered ones.
2026-10-04 16:08:10 +02:00
Local Dev
3c04f971a9 Theseus: warn before opening a name a blocklist flags
The blocklist consumer existed in the resolver library and the gateway, but
the browser opened a flagged name without comment. Now the indexer process
reads the subscribed lists from the chain every ten minutes and hands the
flags to main. A flagged name loads a warning page naming the reason, the
list and the report; the user may continue, and that is remembered per name.

Two gates, because content is reached two ways. loadBns shows the real
interstitial. serveBns refuses with an inline page on every path that skips
it: reload, back and forward, bns:// links, web app windows. The inline page
has no button, since a page at the site's own origin must not be able to
approve itself; only blocked.html may ask to continue, checked by file URL.

Settings › Naming has the policy: warn (default), never open, or ignore the
lists. The csam reason is never offered a way through. A list that cannot be
read keeps the last known flags and never stops a name from resolving.

The gateway put its own warning in front of flagged sites, which this
browser could not get past: it fetches files itself, with no cookie jar. It
now sends x-bns-policy: client and the gateway stays out of the way for a
client that says it decides for itself.

dev/blocklist-selftest.js runs the real protocol handler and decision
functions against a scratch profile.
2026-10-04 15:50:35 +02:00
Local Dev
9f6dd69bf3 registry covenant v1: the contract that holds a name and enforces its lifecycle (draft)
Expiry was always going to be the chain refusing, not resolvers agreeing. This
is that contract: a certificate sits in a covenant whose address encodes its
owner and expiry, and five paths move it. The owner renews, transfers,
releases or migrates; anyone takes over an expired name at a price that
glides from a premium to a floor, paying the previous owner a share while the
grace period lasts and committing to the name a block ahead.

The two changing values are the last constructor arguments, so they sit first
in the redeem script and the contract re-creates itself by swapping 26 bytes.
A resolver that knows the code can check any claimed owner and expiry against
the address.

Every number comes from registry-lifecycle.js, which was written as the
fixture for this. The tests run each path in libauth's VM under the 2025 and
2026 rules, compare price and split to that model along the glide, and then
break one rule at a time with valid signatures; the suite counts that the VM
itself refused them.

Draft, not reviewed, and not wired into any resolver. REGISTRY-COVENANT-v1.md
says where it departs from the design: script cannot bound a height from
above, so reclaim has no deadline and a commit cannot expire, which leaves
commit hoarding open. That is the first thing a reviewer should read.
2026-10-04 15:42:36 +02:00
Local Dev
efea7e2a73 indexer: track the blocklist addresses, so the default list is readable over 443
The project's indexers answered 'no history' for any address but the two
name beacons, so a client on a network that only lets port 443 out could
not read a blocklist at all. The indexer already tracks a set of scripthashes
and follows spends as well as payments; the table's list addresses join that
set.

buildBlocklists still prefers full servers, because a rotated or private
list is unknown to the indexers. It falls back to them only when no full
server can be reached and every subscribed list sits at its table address.
2026-10-04 15:33:25 +02:00
Local Dev
6e6e79f8db gateway: warn before serving a flagged name; expose what the lists say
A flagged name and its subdomains are served behind a warning that names the
reason, the list and the report transaction. The visitor may continue, per
name, for a day. The one refusal is the csam reason.

/api/blocklist returns the subscribed lists and current flags, and
/api/name/<name> carries the flag fields, for clients that cannot reach a
full electrum server. It is a convenience copy: every entry names its report
and can be re-derived from the list address.

Flags are read once a minute from full servers and shared across requests;
names are resolved without the extra round trip. BNS_BLOCKLISTS adds lists.

The mobile constants now carry the default subscriptions for the consumer to
come, and the docs explain flagged names.
2026-10-04 02:33:02 +02:00
Local Dev
dabe9a9f8e blocklist: write side, operator tool, and the tests the roadmap asks for
blocklist-tx.js builds the three payloads and a transaction that takes its
inputs from the list address and returns its change there, which is both the
authority proof and the funding of the next entry. blocklist-admin.mjs
publishes a report and chains the flag on its change, so the pair needs no
waiting; it refuses free-text reasons and declines 'stolen' in this phase.

bns.js recognises the opcodes and reduces a list through the shared reducer
rather than a second copy of the rules.

The tests cover the validity rules, the authority rule against a stranger who
only pays the address, ordering inside one block, and the contract that
matters most: no subscription, or a subscription to an empty list, changes
no answer.
2026-10-04 02:32:59 +02:00
Local Dev
34e336258b bns: the blocklist consumer, shipped with an empty list
A resolver that learns about blocklists only after the first scam needs a
release before it can warn anyone. The consumer ships now, pointed at a list
with no entries, so the first real report costs a transaction instead.

A list is an address, named by a BNS name whose bl record holds it. Entries
are RPT (the report), BLK and UBK. A flag counts only when it names a valid
report in the same list for the same name and reason, and only when its
transaction is spent FROM the list address: anyone can pay an address, so
being in its history proves nothing. That spend rule is what lets the
authority change hands later without the resolver learning the difference.

resolveName attaches blocked, blockReason, blockReportTxid and blockedBy to a
flagged name and leaves every other answer untouched. With no subscription it
is the old resolver exactly. A list that cannot be read never stops a name
from resolving. Lists are read from full electrum servers: the project's
beacon-only indexers answer 'no history' for any other address, which would
look like an empty list.

The default list is sirius-blocklist.x, a two-part name because a three-part
one collapses to its parent. Address conversion is dependency-free so this
file stays a single-file drop-in.
2026-10-04 02:32:58 +02:00
Local Dev
fe62da3e5e sirius-x: release a name from the dashboard; the gateway reports a released name
The Transfer tab gains a Release card. It asks for the name to be typed,
refuses while the name is listed for sale, and says plainly what ends: the
certificate, the records, the ownership, with nothing refunded.

/api/name/<name> returns released_at for a name that left the index through
a release, so a client can tell 'never registered' from 'given back' and
whether the one-block gap has passed. The register flow uses the same signal
to say why a just-released name is not open yet instead of calling it taken.

Docs gain a Releasing a name section; the roadmap marks its first two steps
done.
2026-10-04 01:55:17 +02:00
Local Dev
fcca514c5a registrar: build and send a release, and keep registration closed until it confirms
buildReleaseTx spends the certificate and writes no token output: the
omission is the burn. The certificate's own dust pays the beacon and the fee,
so a release needs no other coin.

releaseWithBuiltInWallet checks whether the certificate sits in a beacon
transaction. One that arrived by a plain transfer cannot be proven burned
from the beacon history, so the flow publishes an UPD first and releases its
output. Both paths ran on chipnet, and a released name was registered again
after its release confirmed.

checkAvailability now reports a pending release as not available: a REG
broadcast while the release is unconfirmed could be mined beside it and be
void, and the buyer would have paid for nothing.
2026-10-04 01:55:14 +02:00
Local Dev
067771cf7f bns.js: the same REL rule, and a parity test over both resolvers
The CLI resolver reduces the beacon history on its own, so a rule that lives
only in resolver-web.js would be a consensus split between the tools that
register names and the clients that resolve them.

release.test.mjs feeds the same synthetic histories to both reducers and
requires identical results: valid and invalid burns, the unanchored
certificate, same-block ordering, the one-block gap in and out of the
mempool, and that nothing of a released registration carries over.
2026-10-04 01:39:15 +02:00
Local Dev
e185ca43a0 bns: REL — an owner can release a name, and resolvers notice a burned certificate
A burned certificate used to leave a name in the index forever: listed,
resolvable with its last records, and impossible to update or re-register.
REL is the declared burn. A transaction that spends the certificate from a
beacon transaction, recreates no output of its category and carries the REL
payload removes the name from the index.

The proof is read from an input because a burn leaves no output to read it
from. Resolvers hold only the beacon history, so the spent output must belong
to a beacon transaction; a certificate moved by a plain transfer is anchored
with an UPD first. That keeps the rule a pure function of the snapshot every
client already has.

A released name can be registered again only in a later block than the
confirmed release. Same-block re-registration would be decided by txid order
between a release and a snipe sitting in the mempool together.

This is the first step of ROADMAP-MAINNET.md §9: it puts burn detection in
the resolvers on its own, before any covenant depends on it. Specified in
PROTOCOL-ADDENDUM-release.md; shared resolver and its Ariadne copy here.
2026-10-04 01:39:14 +02:00
Local Dev
c7c66e1557 Theseus: read BNS names from Ariadne's indexer; its own is the standby
Migration step 3 (DESIGN-bns-indexer-service.md). Ariadne's Thread now owns
BNS indexing on the machine, so Theseus no longer runs a second electrum
indexer beside it.

bns-indexer.js keeps its process and its messages to main.js, but inside
it is now an index host on the shared source chain:
- Ariadne's indexer over its pipe, trusted only after ariadne-helper.exe
  has checked the server process on that connection (found through
  Ariadne's uninstall key), then pushes;
- the local copies: Ariadne's files for both scopes, Theseus's own raw
  copy, the bundled one. The richest wins.
- Theseus's own index copy, written from the pipe data.

The shared core runs as Theseus's own indexer only while Ariadne is
unhealthy. That means: no pipe 4 s after launch, a pipe that fails the
check, a pipe that went silent, or an index not confirmed for 10 min while
Ariadne is not paused. The own indexer warm-starts from Ariadne's
snapshot, so there is no download and no cold sync. It hands back after
90 s of health, so a flapping service does not start and stop it. Economy
is not a failure and never triggers a takeover. With no checkable Ariadne
(portable, not installed, older than the pipe) the own indexer starts at
once, as in 0.3.70. The one thing Theseus does on Ariadne's side is run
the indexer's task at launch when "Launch at start" is off.

main.js passes the shared module paths (packaged as .mjs, which is why the
shared modules no longer import each other) and keeps the host's status.
The Ariadne panel takes its state from the indexer task when one exists,
and the sub-page says where Theseus's names come from.
2026-10-03 17:06:24 +02:00
Local Dev
96d22e86bd BNS indexer core, source chain and pipe protocol as shared modules
Design step 1 (TheseusNavigator/DESIGN-bns-indexer-service.md). Theseus's
bns-indexer.js and Ariadne's daemon each had their own index logic, and
Ariadne's warm-started from the operator's pre-resolved list, which has no
owners. Ariadne's Thread is to own indexing for the whole machine, with
Theseus reading from it, so both need the same code and the same results.

- bns-index-core.js: the 0.3.70 indexer logic without the Electron port:
  local copies -> published raw snapshot (dl.silentmode.st, Sia mirror)
  -> electrum delta every 30 s, index rebuilt from the beacon txs with
  owner, TLDs from tlds.bch. Writes two local copies atomically (current
  + previous), emits change/fresh with the names that changed, and can
  pause its network work (Economy) without stopping.
- bns-source-chain.js: how readers get names without depending on the
  indexer: memory -> live indexer over the pipe -> local copies -> public
  snapshots -> public BNS indexer for single names, marked provisional
  and replaced when verified data arrives.
- bns-pipe.js: newline-JSON hello/snapshot/get/subscribe/poll over
  \.\pipe\ariadne-bns[-<SID>]. Node cannot reach the pipe handle, so the
  client trusts a connection only through a helper that checks the server
  process (GetNamedPipeServerProcessId) and then relays that same
  connection. A pipe name can be squatted while the indexer is down;
  a 127.0.0.1 port cannot be checked at all.

Theseus keeps its own bns-indexer.js unchanged until migration step 3.
2026-10-03 16:01:26 +02:00
Local Dev
e0c420992c Merge branch 'claude/charming-mclean-0c655c' into claude/agitated-bell-bd4ac2 2026-10-03 15:34:05 +02:00
Local Dev
a462441e66 BNS: publish the raw name snapshot publicly, in two copies
The only published raw snapshot sat on the Sia S3 endpoint, which needs
SigV4 for every read: every Theseus launch got a 403, and the copy had not
been refreshed since 2026-08-31 (it was written by hand).

cdn-name-snapshot.mjs runs on the VPS every 10 min: one electrum history
call plus only the new transactions, then writes the same raw format as two
public, read-only copies on two storage backends:
  https://dl.silentmode.st/bns-name-snapshot.json
  https://navigate.st/bns/silentmode.bch/bns-name-snapshot.json  (Sia)
No wallet or credentials are needed to read either; clients still rebuild
the index from the transactions. A failed upload of the Sia copy retries on
the next run. nameListUrl (+ nameListMirrorUrl) point at them.
2026-10-03 13:08:27 +02:00
Local Dev
c78943b35e Ariadne: keep the resolver running on battery
The "BNS Resolver Daemon" and "BNS Sia Bridge" scheduled tasks were
registered with New-ScheduledTaskSettingsSet's defaults, which include
DisallowStartIfOnBatteries and StopIfGoingOnBatteries. On a laptop booted
on battery the system-wide resolver and gateway never started, and an
already-running daemon was killed the moment the charger was unplugged,
so BNS names silently stopped resolving.

install.ps1 (which the Inno installer runs on every install and upgrade)
now registers both tasks with -AllowStartIfOnBatteries
-DontStopIfGoingOnBatteries. The Argus copy of install.ps1 gets the same
change.

Existing installs keep the old task definitions until reinstalled, so the
daemon (running as SYSTEM) now checks both tasks 30 s after start and
flips just those two flags in place via Set-ScheduledTask. Every other
setting and the running instance are left untouched. The fix lands on the
first boot on mains power after the upgrade; battery boots work from then
on.
2026-10-03 13:08:13 +02:00
Local Dev
0a32023a79 Licenses: MPL-2.0 for Theseus, Ariadne and Hephaestus; Apache-2.0 for Argus; CC BY 4.0 for the documents
The public repos carried no license, so nobody could legally copy or build
on the code, and the whitepaper's "free software" had nothing behind it.
Theseus and its companions take the Mozilla Public License 2.0, the
file-level copyleft Firefox and Brave use, which is compatible with every
component they bundle. The resolver and gateway libraries take Apache-2.0
so that other implementations of the registry can reuse them without
copyleft in the way. The protocol documents and the whitepaper are CC BY 4.0.

A third-party notices file lists what the browser ships and fetches, with
the source offer the GPL sing-box binary the VPN add-on downloads requires;
the matching source archive is now published beside the binaries. The
names and marks are reserved in TRADEMARKS.md, separate from the code
license, so a fork must ship under its own name. Settings › General says
the license and links the three files; the whitepaper says the same.
2026-09-29 00:18:00 +02:00
Local Dev
eaf4956f55 Ariadne for Android 0.20: every live TLD claimed; CDN name snapshot refreshed by the gateway
The app read its TLD set from /api/tlds `advertised` — the legacy nine —
so a name under a newer registry TLD was intercepted only once the local
index already carried it. It now reads `routable` (falling back to
`advertised` for an older gateway) and ships a bootstrap list matching
today's routed set for cold start.

The cold-start snapshot it pulls from dl.silentmode.st was produced by
publish-name-mirror.mjs, which needs the operator wallet and ran by hand:
the CDN copy was sixteen days and seven names behind. cdn-name-index.mjs
now reshapes the gateway's own /api/registry into the snapshot format on
the VPS every ten minutes (systemd timer in VPS/SilentMode), so no wallet
and no hand step are involved.
2026-09-28 02:01:55 +02:00
Silent Mode
24b83dee4d feat(theseus): a subdomain rule applies wherever the name is served from
The gateway checks a name's host rules before it decides what to serve, so a
blocked or redirected subdomain behaves the same whatever record the name
carries. Theseus only inherited that for names it proxies through the
gateway's /bns/ mount. A name with both s3 and ip — the shape that caused
the 2026-08-13 subdomain bug — would have had its blocked subdomain answer
anyway, because Theseus talks straight to the IP.

It now asks the gateway for the host's verified rule before taking either of
the paths it serves itself, and only for those paths, so an ordinary
subdomain navigation gains no round trip. Verification stays in one place:
the client reads a decision, it does not re-derive one.

The spec catches up with what is implemented — it still described v1 and
called hosts a future idea.
2026-09-28 01:25:22 +02:00
Silent Mode
1a317007df feat(portal): the name's settings show what its subdomains serve, and let you change it
The per-name tab was a DNS record table: eight record types, of which the
stack reads exactly one, and then only as a last resort when a name carries
no content record. Everything else was signed, stored and read by nobody —
furniture that looked like configuration. Meanwhile the thing a name owner
actually has, a folder tree that already answers on matching subdomains, was
not visible anywhere and could not be changed.

The tab now leads with what answers today: the apex, every folder in the
name's storage next to the subdomain it serves on, and which of those a rule
overrides. Below it, the rules — point a subdomain at a different folder,
redirect it, or switch it off.

Classic DNS keeps its table, behind a disclosure that says plainly what
those records do today and what they are waiting for. Nobody loses a record
they published; they just stop being the first thing a name owner is asked
to think about. The table gains a Host column, so records can belong to a
subdomain rather than only the apex.

Both halves are one signed manifest and one publish button — they always
were one file, and two buttons for it only ever invited a half-saved state.
Labels and values are separated in the generated rows so the words
translate and the owner's own folder and URL do not.

The signer accepted v1 manifests only, which would have rejected every
publish from this screen.
2026-09-28 01:21:47 +02:00
Local Dev
0a698d58d6 Ariadne 0.1.7: system-wide collision policy is runtime-switchable
Feature. The daemon now hot-reloads C:\ProgramData\Ariadne\policy.json every
5 s (fs.stat mtime check, only opens the file when it actually changed) and
flips its DNS collision policy between bcnr-first and icann-first in place.
Every browser on the machine follows within seconds -- no daemon restart,
no reinstall, no UAC. The next Ariadne panel in Theseus will edit this file
directly; a text editor works too.

install.ps1 seeds the file with the current -Policy default on FIRST install
only (an upgrade never clobbers a user's later flip) and grants BUILTIN\Users
Modify on it so an unprivileged app can rewrite it. Invalid values keep the
last good policy (logged). File deleted -> daemon falls back to the CLI
policy the scheduled task was registered with.

Also fixes an intermittent "config unreadable" on daemon startup when
ariadne.config.json had a UTF-8 BOM -- same class of PS-writes-BOM bug 0.1.6
fixed in the TLD-sync path, this time in bnsd.js's own config loader.

Verified live on this box with a second daemon running on port 15353:
  bnsd policy: bcnr-first -> icann-first (from C:\ProgramData\Ariadne\policy.json)
  bnsd policy: icann-first -> bcnr-first (from C:\ProgramData\Ariadne\policy.json)
  bnsd policy: ... has invalid policy 'cash-first' -- keeping bcnr-first
ACL check: BUILTIN\Users has Modify on policy.json, non-elevated write succeeds.

Deployed
- dl.silentmode.st/AriadneResolver-Setup-0.1.7.exe
- s3://bns/silentmode/dl/AriadneResolver-Setup-0.1.7.exe
- silentmode.st manifest advertises 0.1.7
- sha256 742f8005c2dfb47929287c009cddf7738f0d12bd56fb8f8a837df3e9b435f47f

Theseus UI wiring for this file NOT touched -- settings-preload.js has 31
lines of uncommitted work from another session; a Theseus panel selector
belongs in that session's next commit so it doesn't collide.
2026-09-28 01:18:47 +02:00
Silent Mode
9c8b2bfafe feat(gateway): a name's owner decides what its subdomains serve
Until now a subdomain served the folder its labels spelled out, and that was
the only thing it could ever do. There was no way to point one somewhere
else, send one to another site, or stop one answering — the mapping was a
convention with no override.

A verified hosts entry now decides: a different folder, a redirect, or
nothing at all. Block beats redirect beats path, so a host that is both
blocked and redirected stays blocked. The manifest is fetched only when the
request is actually for a subdomain, and the result — including the absence
of one — is cached for a minute, so the apex pays nothing and a name that
never published one pays one lookup per minute rather than one per request.
A manifest that fails to verify is treated exactly like no manifest: the
name keeps serving from its chain records instead of failing shut.

The folder derivation moves next to the picking rule and grows tests,
because getting it wrong does not raise an error — it quietly serves the
wrong bytes. The subtle parts are that labels reverse (DNS reads outward, a
path reads inward) and that a .bch name indexes as <label>.bch even though
normalizeName collapses it to a bare label; matching the bare label would
have served the bucket root for every subdomain of every .bch name.

/api/dns answers for a host too. A host with its own dns block owns its
record set outright, the way classic DNS works; one without inherits the
apex, the way everything else on BNS does.
2026-09-28 01:04:36 +02:00
Silent Mode
98232def96 refactor(gateway): one definition of what makes a signed manifest valid
The signature check was written out twice — once on the read path, once on
the write path — and a third copy was about to be needed for serving. Three
hand-copies of a verification routine is how a hole gets in, and the
dangerous half is subtle: pubkey recovery always succeeds and always yields
some key, so the line that actually proves anything is the comparison of the
derived address to the on-chain owner. That line is now in one place.

The manifest schema gains a hosts block for per-subdomain records and
routing, validated strictly here because whatever we accept we then serve
under the owner's signature: no path traversal, no non-http redirect
targets, no host key that is not a label path. v1 blobs stay valid, so
nothing anyone has published needs republishing.

Also adds the rule for which hosts entry governs a request. The apex is
deliberately never one of them — its content comes from the chain, and
letting an off-chain blob redirect the main site would move a name's trust
boundary without the owner spending a transaction.
2026-09-28 01:01:27 +02:00
Local Dev
8904c1d7ba Ariadne 0.1.5: route and certify every live registry TLD, not the legacy nine
Other browsers stopped opening most BNS names because two lists had
drifted. The installer took its TLD set from the gateway's `advertised`
field — the legacy tlds.bch record, nine TLDs — while the registry had
moved to per-TLD NFTs with twenty (including .x, where most Silent Mode
sites live). And the daemon's permitted-TLD list came from the bundled
ariadne.config.json (seven TLDs) rather than the CA the installer had
just minted, so only the overlap — .bch, .p2p, .deviant, .nav — actually
worked; .sia, .nt and the rest got no certificate, .x got no route at
all.

Now: the gateway publishes `routable` (every live TLD on the public
listing plus the legacy advertised set); the installer routes and
certifies that, skipping ICANN-delegated collision TLDs (.cash, .gt,
.sc — checked against the live IANA root zone, bundled snapshot as
fallback) since the local CA must never be constrained to a real
internet TLD; it writes the certified list into ariadne.config.json;
and the daemon reads its permitted list from the installed CA's own
name constraints, so the two can no longer disagree.
2026-09-27 23:20:26 +02:00
Local Dev
0b6c8093e1 Ariadne 0.1.4: sweep stale BNS root CAs so browsers stop failing after upgrades
Symptom: on any machine that had been through a series of installs, one or
more older "BNS Local Root CA" certs from earlier builds could accumulate in
LocalMachine\Root -- an unconstrained root from before 2026-07, or a rotated
one -- and every install added the current constrained root ALONGSIDE the
stale ones. Two certs with the same Subject-DN defeat browser chain-builders:
Chromium/Edge/Firefox pick one candidate and refuse to try the others, so
every .bch site failed with ERR_CERT_AUTHORITY_INVALID / SEC_ERROR_BAD_SIGNATURE
in every browser EXCEPT Theseus (which trusts the on-chain tls fingerprint
directly, not the machine store).

Reproduced on the dev box: two BNS Local Root CAs present (thumbprints
EB5DBE98... constrained + 23538FD7... unconstrained), DNS answered correctly,
strict Invoke-WebRequest to https://coinspectrum.bch/ failed. Removing the
stale one and re-running the chain-build restored HTTPS 200.

Fix (install.ps1):
- Before Add(imported), enumerate the store and $store.Remove() every
  "BNS Local Root CA" whose thumbprint differs from the one we're about
  to trust. Post-condition: exactly one BNS root remains after install.

Fix (uninstall.ps1):
- Switched from `Get-ChildItem Cert:\LocalMachine\Root` to X509Store direct.
  The Cert: PSDrive is not always loaded in Inno's [UninstallRun] -NoProfile
  context, so uninstall could silently leave the CA behind on the store side
  even while it removed everything else.

Verified end-to-end on this box: 0.1.4 upgrade over 0.1.3 finished cleanly,
BNS Resolver Daemon Running, 127.0.0.1:53 open, exactly one BNS Local Root
CA in the machine store, strict HTTPS to https://coinspectrum.bch/ returned
200 (506,997 bytes) with X509Chain.Build ok=true leaf -> BNS root.

Deployed
- dl.silentmode.st/AriadneResolver-Setup-0.1.4.exe
- s3://bns/silentmode/dl/AriadneResolver-Setup-0.1.4.exe
- silentmode.st manifest advertises 0.1.4
- sha256 30ed5e3bb6d6da5325f854c97976410b4fc63067ed52c810a55e9d2e1bdc4e17
2026-09-27 22:04:00 +02:00
Local Dev
53428a605f bns: one network table instead of chipnet constants copied into fifteen places
Every client hard-coded the chipnet beacons, address prefix and electrum
servers on its own: resolver, registrar config, wallets, gateway, indexer,
mirror scripts, the browser bundle and the mobile Java. A mainnet launch would
have meant finding all of them and hoping none was missed.

The table now lives in resolver-web.js, the one file every client already
shares, so it stays a single-file drop-in. BNS_NETWORK selects the record;
unset means chipnet, so nothing changes today: the live index resolves the
same 60 names and 20 TLDs, the 67 offline tests pass, and the dashboard,
market and studio load the same values through BNS.NETWORK.

The mainnet record carries the verified public servers, the prefix and its
own Sia bucket, but its beacons, start height and operator address are
deliberately null: requireBeacons() refuses to scan until they are pinned in
the order ROADMAP-MAINNET.md §6 requires. Bns.java reads a generated
BnsNetwork.java so the phone cannot drift from the desktop clients.
NETWORK-CONFIG.md records what reads the table and what a launch still pins.
2026-09-27 21:18:37 +02:00
Local Dev
6846d8dde9 gateway: VPN exit catalogue and per-session credential leases
Two routes, in the house flat-if style next to the other /api handlers.

GET /api/vpn/servers returns the exit list with Reality material only —
host, port, sni, fp, pbk, sid — and no client UUID. All of that is
already visible to anyone watching the handshake, so the catalogue is
safe to serve and to cache.

POST /api/vpn/session leases one pre-provisioned UUID from that exit's
pool to the caller for BNS_VPN_TTL_MS and returns a built vless:// URL.
Asking again inside the TTL returns the same lease instead of burning a
second UUID, so a client that reconnects is not counted as a new tenant.
Paid tiers require a signature over BNS-VPN1\n<serverId>\n<tier>\n<ts>
and the recovered address must equal the address the caller claims —
recovery on its own decides nothing. Pro and Max then stop at 501,
because the subscription ledger they would check does not exist yet;
better an honest refusal than a free lease wearing a paid label.

This replaces the shared credential the extension was going to ship. It
does not give real revocation: xray keeps honouring a leased UUID until
the inbound is rotated, so the TTL frees a slot for re-issue but does
not cut a live tunnel. What it does give is that every caller holds a
distinct UUID, so one abuser can be dropped from the pool without
touching anyone else, and nothing is ever published. Real expiry needs
the live xray handler API on each box; noted in the handler comment so
the next session does not assume the TTL is enforced.

The pool file is the only place those UUIDs exist off the servers. It is
read from BNS_VPN_POOL_FILE, created by hand on the VPS, and kept out of
the repo the same way oracle.key is.

One deployment snag found while writing this: the angie vhost forwards
only Host, so every request reaches the gateway as 127.0.0.1 and callers
are indistinguishable. Rather than rate-limit the whole internet as one
bucket — which would let the first user lock out the rest — clientKey()
returns null in that case and the handler answers 503 explaining the
proxy needs X-Forwarded-For.

Smoke test covers what matters for a credential issuer: the catalogue
leaks no UUID, two callers get different ones, one caller asking twice
burns only one, exhaustion is a clean 503, tier gating holds, a junk or
stale signature is refused, and an unidentifiable caller is refused
rather than served. It stages the flat VPS layout so it exercises the
real gateway file, not a rewritten copy. 17/17.
2026-09-27 19:06:24 +02:00
Local Dev
e94399a1d2 market: a dollar listing must not crash the Sell panel, and the gateway must not forget a fresh one
After a successful USD listing the panel tried to show the BCH-style offer
hex, which dollar listings do not have, so the seller saw an error and a
bare Cancel button. The panel now shows the contract address instead.

The gateway pruned listings from its store whenever one electrum lookup did
not show the covenant output. A listing a few seconds old can be invisible
to one server while another already relayed it; deleting it then loses a
real on-chain listing. Absence is now trusted only after 20 minutes; before
that the listing is hidden from buyers but kept.
2026-09-21 23:46:06 +02:00
Local Dev
79a8a6fa16 fix(gateway): 302 to trailing slash on directory-index rescues
/bns/sirius.x/theseus was resolving to theseus/index.html but served in
place under the old URL. The injected <base> then stayed at the parent
directory (/bns/sirius.x/), so the sub-page's own relative references
went wrong in two visible ways:

  * <link rel=icon href="./assets/favicon.svg"> loaded the SIRIUS
    favicon, not the theseus one — visible as the theseus subdirectory
    "borrowing" its parent's icon.
  * href="../market/" inside theseus/index.html resolved to /bns/market/
    (dropping the sirius.x name) — reported as "the market link goes
    to /bns/market/ with usage /bns/./ as result".

Fix: when the directory-index fallback fires, 302 to add the trailing
slash instead of serving in place. The browser follows the redirect to
/bns/sirius.x/theseus/, my per-directory baseHref computes correctly
there, and every relative reference lands where the source expects.

Same behaviour as nginx `try_files ... /$uri/`. Flat-file rescues
(/tld -> tld.html) and SPA fallbacks keep serving in place — those
don't have the base-mismatch problem.

Verified live: /bns/sirius.x/theseus 302 -> /theseus/; theseus's own
favicon loads; ../market/, ../tld, ../portal all stay inside sirius.x.
2026-09-20 20:09:58 +02:00
Local Dev
e130596584 feat(gateway): clean URLs — serve <path>.html + strip .html from hrefs
Two symmetric changes so a site published as flat .html files (sirius.x
has tld.html, portal.html, market.html, register.html, studio.html)
reads with clean URLs to the visitor.

1. S3 handler fallback order for extensionless requests:
     /tld  →  tld.html         (new — first)
     /tld  →  tld/index.html   (existing — second)
     /...  →  index.html       (existing — SPA last-resort)
   Requests with a real file extension (.js, .svg, .json) are never
   rescued — those must never be routed by a client-side router and
   swallowing them would hide real bugs.

2. transformHtml() gains stripHtmlExt(): rewrites href="foo.html"
   (relative) to href="foo", preserving anchor and query. Absolute
   URLs (https://silentmode.st/tlds/) are left alone — external
   references are none of our business.

Backwards-compatible: existing /bns/<name>/foo.html links still 200.
Verified end-to-end on sirius.x: /tld, /portal, /market, /register,
/studio all serve; assets under js/, assets/ unchanged; direct .html
requests still work.
2026-09-20 19:34:42 +02:00
Local Dev
6ebdb98936 feat(market): names can be priced in US dollars — a covenant holds the certificate, a gateway-signed oracle sets the BCH amount
A BCH-priced offer is a signed transaction with a fixed amount, so a
seller who wants "$500" had to relist whenever the rate moved. The new
USD mode moves the certificate into a small CashScript covenant
(contracts/usd-listing.cash). Anyone may buy it by paying the seller
target_cents / price, where the price comes from a message signed by the
gateway's oracle key — the median of five exchanges the site already
uses. The payout is clamped to a floor/ceiling band the seller sets, and
the seller can reclaim the certificate at any time.

What the chain can and cannot enforce, stated plainly in the contract and
the UI: script can require a transaction to be mined no earlier than a
time, never no later, so a quote cannot expire on-chain. The band is the
guarantee, and quotes are bound to the listing's outpoint so cancelling
and relisting voids every quote ever issued. An offline libauth VM test
covers list, buy, underpayment, foreign-listing quotes, forged quotes,
band clamping, cancel and a non-seller cancel; on chipnet aloevera.test
was listed at $0.30, bought by a second wallet for 119,090 sat at
$251.91, listed again by the new owner and cancelled.

Gateway: GET /api/price/oracle[?txid=] serves the oracle public key and
per-listing signed quotes; the key lives in oracle.key beside the
gateway, never in the repo. POST /api/market accepts kind "usd" after
reproducing the redeem script from the stated terms and finding the
certificate in that covenant; such listings are pruned once the covenant
output is spent; DELETE accepts the listing's seller while the index
shows the covenant as holder.

Dashboard: the Sell tab has "Price is fixed in: US dollars / BCH", a
band control with the resulting BCH range, and cancel through the
covenant. Market: USD listings show the dollar price, today's BCH
equivalent and the band; buying fetches a quote and completes the
covenant spend. Studio no longer sends a needless UPD when the registry
view is stale, and missing files on a name now get a real 404 page.
2026-09-20 19:20:46 +02:00
Local Dev
c9102147a7 fix(sirius-x): market page loads the BCH-priced scripts; Sia-served files get a short cache lifetime
The market page had just moved to market/index.html and still referenced
market.js and pricing.js under their previous query strings, so browsers
kept the cached satoshi-priced versions. Tags are now in step across
every page. The gateway also sends cache-control max-age=120 for
everything it serves from Sia; those files carry no ETag, so browsers
were applying heuristic caching and holding old scripts for hours.
2026-09-20 18:38:31 +02:00
Local Dev
63aba73d98 feat(gateway): rewrite cross-name absolute links through /bns/
theseus.x had a hardcoded href="https://sirius.x/" that worked in Theseus
(which resolves BCH names natively) but 404'd for ordinary browsers
served through the relay. Fixing it in every source file each time a
site adds a cross-name link is thankless; fix it once at the gateway.

New transformHtml() = injectBase() + rewriteBnsLinks(). The rewrite
matches href="https?://<label>.<tld>/..." and, when <tld> is a known
BCDN-only TLD, replaces the URL with /bns/<host>/... so the click stays
inside the relay origin.

Allowlisted TLDs: bch, x, p2p, deviant, bit, nav, test, coin, cash.
Intentionally EXCLUDED: st, com, gt, sc, de — those exist in ICANN and
rewriting them would hijack real web references (dl.silentmode.st is
the working case). The rewrite only fires on gateway-served HTML, so
the source sites remain clean when consumed at their native origin
by Theseus or Ariadne.

Verified: https://sirius.x/ on theseus.x home + plug-ins pages now
becomes /bns/sirius.x/; ordinary dl.silentmode.st links unchanged.
2026-09-20 18:35:09 +02:00