blockyard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Auto-update — design
|
|
2
|
+
|
|
3
|
+
**Status: proposal, not implemented.** Written 2026-09-13 at the operator's request: "design an
|
|
4
|
+
auto-update system that integrates with github, so users can upgrade-in-place when notified", with
|
|
5
|
+
a dialog showing "update notes vs last version(s)".
|
|
6
|
+
|
|
7
|
+
Decisions taken by the operator, and designed to here:
|
|
8
|
+
|
|
9
|
+
| question | choice |
|
|
10
|
+
|---|---|
|
|
11
|
+
| autonomy | **notify + one-click apply** — nothing moves without a human |
|
|
12
|
+
| authenticity | **pinned repo over HTTPS, no signatures** |
|
|
13
|
+
| who may apply | **admin; or anyone, when accounts are off** |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. What already exists
|
|
18
|
+
|
|
19
|
+
Three quarters of the notification half is built, and the design leans on it rather than
|
|
20
|
+
duplicating it.
|
|
21
|
+
|
|
22
|
+
- **`computeBuildId(root, version)`** (`server/http/static.js`) hashes every file under `public/`
|
|
23
|
+
by name, size and mtime into `<version>-<10 hex>`. It is recomputed per request, deliberately,
|
|
24
|
+
so it reflects the code *on disk* rather than what the process read at boot.
|
|
25
|
+
- **`GET /api/build`** answers `{ version, build, matchesClient }` and is `auth: 'none'` — the one
|
|
26
|
+
thing a login page is allowed to know.
|
|
27
|
+
- **The browser already polls it** every five minutes (`checkBuild()` in `public/js/app.js`) and
|
|
28
|
+
shows a `stale build — reload` badge in the header (`#buildNote`). This is the notification
|
|
29
|
+
surface; it currently means "the files on disk changed under your tab", and gains a second
|
|
30
|
+
meaning: "a newer release exists upstream".
|
|
31
|
+
- **`Restart=always`** in the systemd unit. This is the single most useful fact in the design: the
|
|
32
|
+
updater never needs `sudo`, `systemctl`, or any privilege at all. It writes files, exits 0, and
|
|
33
|
+
systemd restarts it. (This box happens to have `NOPASSWD: ALL`, and the design must not rely on
|
|
34
|
+
that, because users will not.)
|
|
35
|
+
- **Zero dependencies and no build step.** An update is *replace files and restart*. There is no
|
|
36
|
+
`npm install`, no compile, no native module to rebuild — which removes the largest single source
|
|
37
|
+
of update failure in self-hosted software.
|
|
38
|
+
- **State is already isolated from the code.** `config/local.json`, `config/blockyard.json` and
|
|
39
|
+
`data/` are gitignored, so a `git checkout` cannot touch them. Verified.
|
|
40
|
+
|
|
41
|
+
## 2. Threat model, stated plainly
|
|
42
|
+
|
|
43
|
+
An updater is a mechanism for **running code from the internet as the service account**, on a box
|
|
44
|
+
that holds node RPC credentials and can reach a Bitcoin node. That deserves naming before any
|
|
45
|
+
mechanism is chosen.
|
|
46
|
+
|
|
47
|
+
What the chosen options do and do not defend against:
|
|
48
|
+
|
|
49
|
+
| threat | covered? |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Network attacker tampering in transit | **Yes** — HTTPS with certificate validation, pinned remote URL. |
|
|
52
|
+
| Someone pointing the updater at a different repo | **Yes** — the remote URL is pinned in code, not read from config. |
|
|
53
|
+
| Downgrade to a known-vulnerable older release | **Yes** — fast-forward-only; see §4. |
|
|
54
|
+
| History rewrite / force-push on the release branch | **Yes** — fast-forward-only refuses it. |
|
|
55
|
+
| **Compromise of the GitHub account or repo** | **No.** Without signatures, a malicious push is indistinguishable from a release. This is the accepted residual risk of the "no signatures" choice. |
|
|
56
|
+
| **A stranger on the LAN triggering an update** | **Not in open mode** — see the objection in §6. |
|
|
57
|
+
|
|
58
|
+
Signed tags remain the upgrade path if that residual risk ever stops being acceptable; §9 says what
|
|
59
|
+
would change.
|
|
60
|
+
|
|
61
|
+
## 3. Release channel
|
|
62
|
+
|
|
63
|
+
Releases are **annotated git tags** matching `v<major>.<minor>.<patch>` on the pinned remote. Not
|
|
64
|
+
branch heads: a branch tip is whatever was pushed last, including a half-finished afternoon.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
origin https://github.com/BobClawblaw/blockyard.git (pinned in code)
|
|
68
|
+
tag v0.0.9, v0.1.0, ...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The updater does a **shallow tag fetch** (`git fetch --tags --depth=1 origin`) — the repository is
|
|
72
|
+
14 MB today, but a user's machine should not pay for history it will never read.
|
|
73
|
+
|
|
74
|
+
> **Blocker for public use:** the repository is **private**. `git fetch` from a user's deployment
|
|
75
|
+
> needs credentials that do not exist. On this box it works only because `gh` puts a `GH_TOKEN` in
|
|
76
|
+
> the environment — there is no credential helper configured. **The updater is unusable by anyone
|
|
77
|
+
> else until the repository is public**, and no amount of design fixes that. This is the first
|
|
78
|
+
> thing to resolve before implementing.
|
|
79
|
+
|
|
80
|
+
## 4. Mechanism
|
|
81
|
+
|
|
82
|
+
### 4.1 Check (cheap, periodic, read-only)
|
|
83
|
+
|
|
84
|
+
Every `update.checkEveryMs` (default 6 h, `0` disables), and on demand:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
git ls-remote --tags --refs origin # one network call, no objects fetched
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pick the highest tag by semver that is **greater than the running version**. Cache
|
|
91
|
+
`{ latest, checkedAt, notes }` in memory and in `data/update-state.json`.
|
|
92
|
+
|
|
93
|
+
*No GitHub API, no token, no rate limit to respect* — `ls-remote` is the same transport as the
|
|
94
|
+
fetch, and works for a private repo with whatever credential the user configured.
|
|
95
|
+
|
|
96
|
+
### 4.2 Notes for the dialog
|
|
97
|
+
|
|
98
|
+
The operator asked for "update notes vs last version(s)" — plural, which matters: someone three
|
|
99
|
+
releases behind should see all three, not just the newest.
|
|
100
|
+
|
|
101
|
+
Notes come from **`CHANGELOG.md` at the target tag**, not from the GitHub Releases API. The reason
|
|
102
|
+
is integrity, not convenience: the notes then come from the same pinned source as the code, so they
|
|
103
|
+
cannot disagree with what is about to be installed.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
git fetch --depth=1 origin tag v0.1.2
|
|
107
|
+
git show v0.1.2:CHANGELOG.md
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Parse the `## [x.y.z] — date` sections, take every section newer than the running version, and
|
|
111
|
+
return them as structured entries:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{ "current": "0.1.0", "latest": "0.1.2", "behind": 2,
|
|
115
|
+
"releases": [
|
|
116
|
+
{ "version": "0.1.2", "date": "2026-09-20", "sections": { "Fixed": ["..."] } },
|
|
117
|
+
{ "version": "0.1.1", "date": "2026-09-16", "sections": { "Added": ["..."], "Fixed": ["..."] } }
|
|
118
|
+
] }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`CHANGELOG.md` already has the required shape (`## [0.0.9] — 2026-09-14`). Markdown is rendered as
|
|
122
|
+
**text, not HTML** — see §7.
|
|
123
|
+
|
|
124
|
+
### 4.3 Apply
|
|
125
|
+
|
|
126
|
+
1. **Refuse if the working tree is dirty.** `git status --porcelain` must be empty. An operator who
|
|
127
|
+
edited a file locally gets told so, with the file list, rather than having the edit destroyed or
|
|
128
|
+
the updater silently refusing forever. This is a real case: this deployment has been edited in
|
|
129
|
+
place repeatedly.
|
|
130
|
+
2. **Refuse if not fast-forward.** `git merge-base --is-ancestor HEAD <tag>` must succeed. This is
|
|
131
|
+
what makes downgrade and force-push rewrite impossible without a human on the box.
|
|
132
|
+
3. **Record the rollback point** — `git rev-parse HEAD` into `data/update-state.json`, with the
|
|
133
|
+
version and timestamp, *before* anything moves.
|
|
134
|
+
4. **Check out the tag** — `git -c advice.detachedHead=false checkout --detach <tag>`.
|
|
135
|
+
5. **Verify what landed** — `git rev-parse HEAD` matches the tag's commit, and `package.json`'s
|
|
136
|
+
version matches the tag. A mismatch aborts and rolls back immediately.
|
|
137
|
+
6. **Audit** the whole thing: `{ type: 'update', from, to, user, ip, ok }`.
|
|
138
|
+
7. **Exit 0.** systemd restarts the process on the new code. The browser's existing five-minute
|
|
139
|
+
`checkBuild()` poll notices the new build and shows the reload badge — no new client machinery.
|
|
140
|
+
|
|
141
|
+
There is deliberately **no `npm install` step**, because there are no dependencies. If that ever
|
|
142
|
+
changes, this design needs revisiting, not extending.
|
|
143
|
+
|
|
144
|
+
### 4.4 Rollback
|
|
145
|
+
|
|
146
|
+
`POST /api/update/rollback` checks out the recorded previous commit and exits 0. One click, and it
|
|
147
|
+
works even if the new version cannot serve a page, because the *previous* process is what is
|
|
148
|
+
running by then. A version that fails to boot is caught by systemd's restart loop; the rollback
|
|
149
|
+
point on disk is what an operator uses from the shell (`git checkout <sha>`), and the docs must say
|
|
150
|
+
so, because a UI cannot rescue a server that will not start.
|
|
151
|
+
|
|
152
|
+
## 5. API surface
|
|
153
|
+
|
|
154
|
+
| route | auth | purpose |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `GET /api/update` | `any` | `{ current, latest, behind, releases[], checkedAt, dirty, canApply, reason }` |
|
|
157
|
+
| `POST /api/update/check` | admin-or-open | Force a check now. Rate limited. |
|
|
158
|
+
| `POST /api/update/apply` | admin-or-open | Body `{ version, confirm: "<version>" }`. |
|
|
159
|
+
| `POST /api/update/rollback` | admin-or-open | Body `{ confirm: "rollback" }`. |
|
|
160
|
+
|
|
161
|
+
`confirm` must equal the **version being installed** — the same shape as the node-connection form's
|
|
162
|
+
`confirm: "save"`, but carrying the target, so a replayed or cross-site request cannot apply a
|
|
163
|
+
different release than the one the dialog showed.
|
|
164
|
+
|
|
165
|
+
## 6. The open-mode objection
|
|
166
|
+
|
|
167
|
+
The operator chose "admin; or anyone, when accounts are off", matching `configWriteAllowed`. The
|
|
168
|
+
design implements that, and records the objection, because it contradicts the project's own
|
|
169
|
+
strictest existing rule.
|
|
170
|
+
|
|
171
|
+
`actionAllowed()` in `server/rpc/allowlist.js` refuses node writes in open mode *even when the
|
|
172
|
+
operator has explicitly listed them*, with this reasoning:
|
|
173
|
+
|
|
174
|
+
> accounts are off, so there is no identity to hold a node write accountable
|
|
175
|
+
|
|
176
|
+
A node write is one RPC call. **An update replaces the entire codebase and restarts the process.**
|
|
177
|
+
If the stricter rule is right for `savemempool`, it is hard to argue it is wrong here. Under the
|
|
178
|
+
chosen setting, on a default install, anyone who can reach port 21000 can make the server fetch and
|
|
179
|
+
execute a new release.
|
|
180
|
+
|
|
181
|
+
Mitigations that preserve the operator's choice:
|
|
182
|
+
|
|
183
|
+
- **Only tagged releases** — not arbitrary commits or branches.
|
|
184
|
+
- **Fast-forward only** — no downgrade, no rewritten history.
|
|
185
|
+
- **Typed confirmation** carrying the target version.
|
|
186
|
+
- **Cross-site refused** — the `Origin`/`Sec-Fetch-Site` check added 2026-09-13 already covers open
|
|
187
|
+
mode, so a drive-by page cannot trigger it; the exposure is to someone who can reach the port.
|
|
188
|
+
- **Audited**, with the applying identity (or `anonymous`) and address.
|
|
189
|
+
- **Rate limited**, one apply in flight at a time.
|
|
190
|
+
|
|
191
|
+
If that residual is not acceptable later, the one-line change is an
|
|
192
|
+
`update.allowWithoutAuth` flag defaulting to `false`, mirroring `actions.allowWritesWithoutAuth` —
|
|
193
|
+
which is exactly how the project already resolved this same argument once.
|
|
194
|
+
|
|
195
|
+
## 7. UI
|
|
196
|
+
|
|
197
|
+
**Notification.** The existing `#buildNote` badge gains a second state: `update available — v0.1.2`.
|
|
198
|
+
It already polls every five minutes and already handles "server unreachable" without lying.
|
|
199
|
+
|
|
200
|
+
**Dialog.** Reuse the settings modal's structure (`.cfgwrap` / `.cfgscrim` / `role="dialog"
|
|
201
|
+
aria-modal="true"`), which is the overlay pattern that already renders correctly in Safari after
|
|
202
|
+
the 2026-09-12 fix. Contents:
|
|
203
|
+
|
|
204
|
+
- current version → target version, and how many releases are being skipped;
|
|
205
|
+
- **one collapsible block per intervening release**, newest first, with its date and its
|
|
206
|
+
Added/Changed/Fixed sections;
|
|
207
|
+
- what will happen, in words: the server restarts, settings and history are untouched, the page
|
|
208
|
+
reloads itself;
|
|
209
|
+
- any blocker, stated before the button: a dirty tree lists the modified files;
|
|
210
|
+
- the confirm field, and **Update** / **Cancel**.
|
|
211
|
+
|
|
212
|
+
**Rendering the notes is a security boundary.** `CHANGELOG.md` is attacker-influenced input in the
|
|
213
|
+
threat model where the repo is compromised. It is rendered as **escaped text** through
|
|
214
|
+
`fmt.esc()` — no markdown-to-HTML, no `innerHTML` of remote content. The CSP has no
|
|
215
|
+
`unsafe-inline` and `csp.test.js` enforces that; this must not become the exception.
|
|
216
|
+
|
|
217
|
+
## 8. Configuration
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{ "update": {
|
|
221
|
+
"enabled": true,
|
|
222
|
+
"checkEveryMs": 21600000,
|
|
223
|
+
"channel": "stable"
|
|
224
|
+
} }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The remote URL is **not** configurable — it is pinned in code. Making it a config value would turn
|
|
228
|
+
a config-file write into arbitrary code execution, and this app has a config-writing endpoint.
|
|
229
|
+
|
|
230
|
+
## 9. What would change with signatures
|
|
231
|
+
|
|
232
|
+
If the residual risk in §2 becomes unacceptable: sign release tags, commit the public key to the
|
|
233
|
+
repo, and add `git verify-tag` before step 4.4.4. No new dependency (`git` verifies), no change to
|
|
234
|
+
the transport, no change to the UI. The cost is release ceremony — every release must be signed, and
|
|
235
|
+
an unsigned tag must fail closed.
|
|
236
|
+
|
|
237
|
+
## 10. Prerequisites before implementation
|
|
238
|
+
|
|
239
|
+
1. **Make the repository public**, or document that auto-update requires a token. Today it is
|
|
240
|
+
private (still so on 2026-09-14) and the feature cannot work for anyone else.
|
|
241
|
+
2. ~~**Fix the version scheme.**~~ **Done 2026-09-13, renumbered 2026-09-14.** `package.json` and
|
|
242
|
+
`server/main.js` said **0.0.9** while `CHANGELOG.md` said **[0.9.0]** and **[0.1.0]**; any "is
|
|
243
|
+
this newer?" comparison would have been wrong, and that field is what the whole feature
|
|
244
|
+
compares. The operator settled the initial release number as 0.0.9 on 2026-09-14, and every
|
|
245
|
+
source says so. The version is no longer written down twice: `server/main.js` reads it from
|
|
246
|
+
`package.json`, and `test/version.test.js` pins the CHANGELOG's newest release and the README
|
|
247
|
+
to it.
|
|
248
|
+
3. ~~**Create the first tag.**~~ **Done 2026-09-14:** `v0.0.9`, the initial release, is
|
|
249
|
+
tagged, so the release channel in §3 exists.
|
|
250
|
+
4. **Decide the open-mode question** in §6 knowingly.
|
|
251
|
+
5. **Decide the npm path.** Since 2026-09-14 BlockYard is also an npm package with a `blockyard`
|
|
252
|
+
command (`bin/blockyard.js`), keeping its config and data under `~/.blockyard`. A global npm
|
|
253
|
+
install has no git checkout to fetch into, so nothing in §4 applies to it; for that install
|
|
254
|
+
the update is `npm install -g blockyard@<version>`, and the dirty-tree, fast-forward and
|
|
255
|
+
rollback guarantees would have to be restated in npm's terms or the feature limited to
|
|
256
|
+
checkouts. This design covers the checkout only until that is decided.
|
|
257
|
+
|
|
258
|
+
## 11. Tests
|
|
259
|
+
|
|
260
|
+
- semver comparison, including the 0.0.9 / 0.9.0 trap and pre-release suffixes;
|
|
261
|
+
- changelog parsing across several releases, and against a malformed changelog;
|
|
262
|
+
- fast-forward refusal on a non-ancestor tag;
|
|
263
|
+
- dirty-tree refusal, naming the files;
|
|
264
|
+
- confirm-mismatch refusal;
|
|
265
|
+
- rollback restores the recorded commit;
|
|
266
|
+
- notes containing `<script>` render as text (the §7 boundary);
|
|
267
|
+
- open-mode cross-site apply is refused;
|
|
268
|
+
- **a negative control for each**: every one of these must fail against the unfixed behaviour, or
|
|
269
|
+
it is decoration.
|