tosijs-ui 1.9.0-rc.2 → 1.9.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +277 -39
  2. package/bin/deploy-preview.ts +78 -65
  3. package/bin/make-icon-data.js +93 -2
  4. package/bin/release-notes.ts +150 -0
  5. package/bin/resolve-site-config.ts +151 -1
  6. package/bin/tunnel.ts +99 -44
  7. package/deploy/Caddyfile +24 -13
  8. package/deploy/build-index.sh +8 -2
  9. package/dist/doc-browser.js +2 -1
  10. package/dist/doc-system/book-target.d.ts +51 -0
  11. package/dist/doc-system/book-target.js +136 -0
  12. package/dist/doc-system/release-notes.d.ts +33 -0
  13. package/dist/doc-system/release-notes.js +199 -0
  14. package/dist/doc-system/site/audit-guard.js +13 -6
  15. package/dist/doc-system/site/dev-auth.d.ts +32 -1
  16. package/dist/doc-system/site/dev-auth.js +71 -1
  17. package/dist/doc-system/site/dev-server.d.ts +8 -1
  18. package/dist/doc-system/site/dev-server.js +163 -39
  19. package/dist/doc-system/site/docs.d.ts +23 -0
  20. package/dist/doc-system/site/docs.js +17 -1
  21. package/dist/doc-system/site/epub.d.ts +9 -0
  22. package/dist/doc-system/site/epub.js +55 -4
  23. package/dist/doc-system/site/open-browser.js +10 -2
  24. package/dist/doc-system/site/orchestrator.js +57 -5
  25. package/dist/doc-system/site/site-config.d.ts +44 -21
  26. package/dist/doc-system/site/site-config.js +10 -0
  27. package/dist/icon-data.js +314 -314
  28. package/dist/icon-svg.js +20 -2
  29. package/dist/icons.js +4 -2
  30. package/dist/iife.js +12 -4
  31. package/dist/iife.js.map +9 -9
  32. package/dist/live-example/component.js +2 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/dist/value-renderer.js +11 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/llms.txt +5 -1
  38. package/package.json +16 -12
package/CHANGELOG.md CHANGED
@@ -1,6 +1,249 @@
1
1
  # Changelog
2
2
 
3
- ## 1.9.0-rc.2
3
+ ## 1.9.0
4
+
5
+ Consumer repairs, remote access, and **authoring: many books from one corpus**.
6
+
7
+ **Anyone on 1.8.0 should upgrade, whether or not they want the new remote-editing
8
+ feature.** 1.8.0's `tosijs-ui/site` entry point does not import at all in a clean
9
+ install — `index.js` statically re-exports `devServer`, which imports `chokidar` at top
10
+ level, and `chokidar` was declared only in `devDependencies` (#32). 1.8.0 also shipped
11
+ 5.2 MB / 16 files of doc-site hydrate bundle into every consumer's `dist/` (#31), and
12
+ broke the `tosijs-make-icons` bin (#30). All three are fixed here.
13
+
14
+ ### Consumer repairs
15
+
16
+ - **`tosijs-ui/site` imports in a clean install again** (#32). `chokidar` is now an
17
+ optional peer, imported lazily by the watch path only — a plain `buildSite()` never
18
+ reaches for it.
19
+ - **The library package no longer carries the doc-site hydrate bundle** (#31). It built
20
+ into `dist/hydrate/` and shipped to everyone though nothing references it; one adopter's
21
+ package went from 0.62 MB / 398 files to **10.2 MB / 2888 files**. It now builds in a
22
+ temp dir. Caught only by reading `npm pack` output, which is why there is now a lane
23
+ that does exactly that (`bun run test-consumer`).
24
+ - **`tosijs-make-icons` no longer strips `fill-rule`** (#30), which rendered holes in
25
+ compound `evenodd` paths solid — a keyboard drawn as one path came out filled. It was
26
+ stripped in **three** places, not one. `fill-rule` is fill _topology_, not colour, and
27
+ takes no part in the tinting the strip exists for. Ships as a bin, so it affects anyone
28
+ generating their own icon data.
29
+ - **The shipped bins have shebangs** (#35, #36 — reported independently by both adopters).
30
+ Without one, `node_modules/.bin` shims hand TypeScript to the shell.
31
+ - **`/version.json`'s `generator` reports tosijs-ui's version, not the consumer's** (#37).
32
+ It read `package.json` from the _cwd_, so the one field that answers "which tosijs-ui
33
+ built this?" named the wrong package entirely.
34
+ - **A failed "save to source" says why** (#34). The server answers 501 with an actionable
35
+ reason and the client discarded it for a generic "Save failed.", so an unconfigured
36
+ server was indistinguishable from a broken one and the edit appeared to vanish.
37
+ - **New: `iconSvg(name)` / `iconNames()`** (#33) — raw SVG markup with **no DOM
38
+ required**, for build scripts, ePub passes and server-rendered templates. `defineIcons()`
39
+ could write to the icon map but nothing could read it.
40
+
41
+ ### Remote access — view and edit your dev server from anywhere
42
+
43
+ `preview.tunnel` exposes the dev server running on your machine at an authenticated public
44
+ URL over an SSH reverse tunnel, so you can read and edit real source from a phone or a
45
+ borrowed laptop. The box does no compute — it terminates TLS and routes — which is what
46
+ lets one small VPS front many projects. Two hostnames make the posture legible:
47
+
48
+ | host | what it is | gate |
49
+ | -------------------------------- | ----------------------------- | --------------- |
50
+ | `<project>.dev.example.com` | read-only snapshot, shareable | invite cookie |
51
+ | `<project>.edit.dev.example.com` | live workspace, yours | session, always |
52
+
53
+ `tosijs-tunnel --link` prints a single-use link; opening it once exchanges the token for a
54
+ durable `HttpOnly; Secure; SameSite=Lax` session cookie and redirects with the token
55
+ stripped, so it never lands in history or a `Referer`. `tosijs-deploy` publishes the static
56
+ snapshot and self-registers a Caddy fragment, so adding a project edits no shared file.
57
+
58
+ **Write authorization keys on the LISTENER, never on a peer address or a header.** Tunnel
59
+ traffic arrives on a dedicated loopback listener, and anything arriving there needs a valid
60
+ session — because a reverse tunnel counterfeits "local" by construction.
61
+
62
+ ### Security
63
+
64
+ - The tunnelled workspace's **read** gate keyed on `X-Forwarded-*` rather than the
65
+ listener, so any forwarder that omits those headers — `ssh -R` with `GatewayPorts yes`,
66
+ `ngrok tcp`, `socat`, iptables DNAT, nginx `proxy_pass`, HAProxy without
67
+ `option forwardfor` — read the entire uncommitted working tree unauthenticated, while
68
+ `requireToken` promised "nothing at all, not even the page". Now keyed on the listener,
69
+ like every other gate.
70
+ - `POST /report` was unauthenticated and reachable through the tunnel — a stranger could
71
+ fabricate `{passed:N, failed:0}` and make the test lane **exit green on a suite that
72
+ never ran**. Now local-only.
73
+ - Build-error text (absolute paths from your machine) was injected into every served page
74
+ with no auth check. The label stays public; the detail needs a session.
75
+ - `rsync --delete` accepted any absolute path two segments deep — including `/usr/lib` and
76
+ `/etc/caddy`, which it would have _mirrored_, i.e. emptied. Now an allowlist of preview
77
+ roots, and never the root itself: one dropped path segment would otherwise have deleted
78
+ every other project on a shared box, including the Caddy fragments that route them.
79
+ - `tunnel.requireToken` defaults to **`true`**. The hostname is not a secret — Let's
80
+ Encrypt publishes every certificate to public CT logs — so the session gate carries the
81
+ weight. See the docs for what the hostname discloses and what to do about it (#38).
82
+
83
+ ### Fixed
84
+
85
+ - **A 403 could destroy uncommitted work.** An unauthorized read fell through to GitHub
86
+ `main`, so you edited the _published_ file, and the save then handed you a download of
87
+ it — applying which silently reverts your working copy.
88
+ - **`?t=` was intercepted on every request and method**, so any dev server answered
89
+ `GET /?t=12345` with "that invite link has been used" instead of the page, and 401'd
90
+ POSTs carrying `t`. (`t` is the classic cache-buster name.)
91
+ - **Tunnel ports are derived, not fixed** (#39). The bin fell back to a hard-coded 8788
92
+ while the server used `PORT + 1`; they agreed only when `PORT` was 8787. `remotePort` is
93
+ now derived per project too, so two projects on one host cannot collide. One resolver
94
+ serves both, and `tosijs-tunnel --status` reports the ports it would use.
95
+ - **The last-good build protection moved to where the wipe is** — it lived in the dev
96
+ server's watch branch, so `bun run build`, CI, adopters and `bun start`'s own initial
97
+ build had none of it. A red `tsc` no longer discards a freshly generated site either.
98
+ - Two dev servers can coexist; the idle-exit timer counts requests again, not just builds;
99
+ "Build failed" is no longer overwritten by test results seconds later.
100
+
101
+ ### ⚠️ `hidden` now actually withholds
102
+
103
+ **If you have been using `draft: true` or `hidden: true` for working notes or unfinished
104
+ chapters, they were public.** `hidden` only removed a doc from the nav and the book — its
105
+ full text was still written into `docs.json`, and it still got a pre-rendered page at its
106
+ own URL. The code comment claimed "drafts don't ship"; a probe confirmed they did, twice
107
+ over.
108
+
109
+ `hidden: true` now means **not published at all**: absent from `docs.json`, from the
110
+ generated pages, from every book, and from `llms.txt`. It is filtered at _extraction_,
111
+ because the corpus is the thing that gets published and any filter applied after it is
112
+ written is too late.
113
+
114
+ It is inherited, and a child **cannot** un-hide itself from a hidden parent — accidentally
115
+ publishing one chapter of a withheld section is the failure worth preventing.
116
+
117
+ ### Books
118
+
119
+ A `book` value on a doc selects which volume it binds into. Volumes are discovered from
120
+ the corpus, so a second book needs no configuration:
121
+
122
+ ```text
123
+ <!--{ "book": "field-guide" }--> → its own volume
124
+ <!--{ "book": ["default", "field-guide"] }--> → bound into both
125
+ <!--{ "book": "none" }--> → on the site, in no book
126
+ ```
127
+
128
+ Like `hidden`, it is inherited down the `parent` chain — you mark a section, not every
129
+ leaf — with the **nearest declaration winning outright**, so a chapter can divert to
130
+ another volume, join several, or opt out. A list is what gets you shared front matter: a
131
+ glossary or licence page bound into every volume from one source file rather than copied
132
+ per book. `"none"` anywhere in a list wins, because a contradiction should resolve to
133
+ withholding. `"default"` exists as a writable name for the main volume precisely so a list
134
+ can include it — without one, an array could only ever express "not the default".
135
+
136
+ `config.book` (the manifest that curates and orders docs _within_ a volume) and a doc's
137
+ `book` metadata (_which_ volume) are different things that share a word; both are
138
+ documented.
139
+
140
+ ### Release notes assemble themselves
141
+
142
+ `tosijs-release-notes` builds a CHANGELOG section from `[tag]` bullets in commit bodies,
143
+ so notes are written next to the diff rather than reconstructed from memory at the end —
144
+ which is where three false claims in the 1.9.0 notes came from. One bullet per
145
+ separately-interesting _thing_, layered on the conventional subject rather than replacing
146
+ it: a `type:` prefix can only ever represent one change, and the 1.9.0 review commit did
147
+ ten things.
148
+
149
+ tosijs-release-notes # assemble the section
150
+ tosijs-release-notes --check # gate: nothing since the last tag is unaccounted for
151
+
152
+ `--check` fails when an annotation is missing from the changelog, and separately reports
153
+ any commit whose `[fix]`/`[new]` bullets are contradicted by a **markdown-only diff** —
154
+ the shape of the worst 1.9.0 bug, where an auth-gate fix was asserted by a commit whose
155
+ diff never touched the file it named. Matching is deliberately loose: rewriting a bullet
156
+ into prose is the intent, and demanding a literal match would only train people to paste.
157
+
158
+ This section was assembled by the tool and then written up by hand, which is the intended
159
+ workflow — it generates a skeleton and a coverage gate, never the prose. The gate then
160
+ caught two things this very section had dropped, including a bug found by running the tool
161
+ over this repo's own 2026-04/05 history: a bullet used as the commit _subject_ swallowed
162
+ the entire body as continuation text, so one note came out as three paragraphs of
163
+ implementation detail.
164
+
165
+ ### Also
166
+
167
+ - `tosijs-make-icons` emits clean, stable, formatter-conformant output, and honours your
168
+ prettier config if prettier is resolvable. Generated icon data no longer churns against
169
+ a formatter (and should still be added to `.prettierignore`).
170
+ - `resolveBook` → `resolveBooks`, returning a list. New in this release; no migration.
171
+
172
+ ### Late fixes (rc.3 → rc.4)
173
+
174
+ Three review passes ran against this release. The last two found defects **introduced by
175
+ the previous pass's fixes**, which is why these are listed separately rather than folded
176
+ in silently.
177
+
178
+ **⚠️ `deploy/Caddyfile` is now a TEMPLATE — action required if you installed it.** It
179
+ shipped in every tarball carrying a real ACME email, a specific preview domain, and a
180
+ literal `__PREVIEW_TOKEN__`, while its own header told you to `scp` it to `/etc/caddy`.
181
+ Following that gave you a preview host whose entire invite gate was a string published in
182
+ a public repo, issuing certificates under someone else's Let's Encrypt account. Set
183
+ `PREVIEW_TOKEN`, `ACME_EMAIL` and `PREVIEW_DOMAIN` in `/etc/caddy/preview.env` and
184
+ re-install with `deploy:caddy`, which now substitutes all three, **refuses to install if
185
+ any placeholder survives**, and validates into a temp file before replacing the live
186
+ config rather than after.
187
+
188
+ - **Every ePub volume shipped an identical `dc:identifier`, `dc:title` and cover.**
189
+ `unique-identifier` is EPUB3's primary key, so importing two volumes into Apple Books
190
+ or Calibre made the second replace the first — you would silently lose a book. Volumes
191
+ now derive their own identity; `epub.volumeTitles` names them properly.
192
+ - **An ePub volume matching no documents shipped as a success** — a typo'd book name
193
+ produced a valid ~10KB book with a cover, a nav and no chapters, at exit 0. It now fails
194
+ and lists the volumes that do exist. Relatedly, a corpus where _every_ doc names a
195
+ volume no longer errors on its empty default bucket.
196
+ - **An invite link was unredeemable** on a proxied dev server with no `preview.tunnel`
197
+ block: the lock armed off a config section that did not exist while the `?t=` reader,
198
+ correctly gated on it, refused to look. One predicate now, exported and tested.
199
+ - **`tosijs-tunnel` ignored `PORT` and `--port`**, targeting one port while the server
200
+ listened on another — the drift #39 was meant to end, reintroduced by the commit that
201
+ claimed to end it.
202
+ - **The deploy and tunnel bins printed ✅ over failed remote steps.** `tosijs-deploy` now
203
+ distinguishes _deployed_ from _routed_ and exits non-zero when the files landed but
204
+ nothing serves them; `tosijs-tunnel` marks a public URL that is not routed.
205
+ - **Preview roots other than `/srv/preview` registered successfully and routed nothing** —
206
+ the write side was de-hardcoded while the Caddyfile glob and index script still named
207
+ the default. One place decides now, and a mismatch says so. Override with
208
+ `preview.caddySitesDir`.
209
+ - **`release-check` cleared an empty range** at exactly the release boundary, because
210
+ `git describe` returns the nearest tag including prereleases. It now baselines on the
211
+ last stable release _this commit descends from_, so a hotfix tagged out of order or a
212
+ maintenance branch cannot mislead it.
213
+ - **Publishing now gates on `release-check`.** It was added mid-cycle, and the first thing
214
+ it caught was nine unrecorded annotations including a `[break]` — the `deploy/Caddyfile`
215
+ change above, which would otherwise have shipped with no migration note at all.
216
+ - **The release gate hardened against itself.** `release-check` is now baselined on the
217
+ last stable release this commit _descends from_ (`git describe --exclude`), so a hotfix
218
+ tagged after a later release, or a maintenance branch, cannot mislead it — a repo-wide
219
+ tag scan got both wrong. Its coverage matcher also stopped crying wolf: it filtered
220
+ short words out of the annotation but not out of the changelog, so entries that were
221
+ plainly written up got reported as missing.
222
+ - **`isLockedDown()` and `hasTunnel()` are exported** from the auth module. The predicate
223
+ existed in three places, one of them recomputed inside its own regression test — so
224
+ reverting the fix left the entire suite green. The test imports the real function now.
225
+ - **The browser-test lane keys on haltija's `ready` signal** (haltija ≥ 1.6.1, added in
226
+ response to us filing it): a server that is up but has no connected tab is no longer
227
+ adopted, because `hj navigate` then fails with "no browser reachable". That cost three
228
+ lane runs in a day, each needing a manual `pkill`. The floor also named 1.5.5, a version
229
+ that was never published.
230
+ - **New "Host bootstrap" docs**, because the preview host needs setting up once before
231
+ any project can register. Both bins write fragments that `import preview_site` /
232
+ `import tunnel_site` — snippets that only exist once the host has a Caddyfile — so an
233
+ un-bootstrapped box failed `caddy validate` forever, for every project, silently. That
234
+ failure now names the missing prerequisite.
235
+ - `epub.volumeTitles` is now accepted by `defineSiteConfig` — it was documented and
236
+ type-rejected. `extractDocs`'s hidden-filtering gained tests at the tier that matters:
237
+ real files, real entry point, verified by mutation.
238
+
239
+ ### For maintainers of adopting projects
240
+
241
+ `bun run test-consumer` packs the tarball, installs it into a scratch project, and builds
242
+ from that project's cwd. Every other lane runs _in this repo, from this repo, with one dev
243
+ server_ — and four regressions shipped anyway, each living outside exactly that envelope.
244
+ More unit tests would have caught none of them; the gap was context, not depth.
245
+
246
+ ### 1.9.0-rc.2 (prerelease detail)
4
247
 
5
248
  Fixes everything both consumers hit within minutes of `rc.1`. Three were regressions
6
249
  introduced by rc.1 itself.
@@ -21,8 +264,8 @@ introduced by rc.1 itself.
21
264
  broken".
22
265
  - **New lane: `bun run test-consumer`** — packs the tarball, installs it into a scratch
23
266
  project, runs every bin through the `node_modules/.bin` shims, and builds a site from
24
- that project's cwd. Every other lane runs *in this repo, from this repo, with one dev
25
- server*, and all four rc.1-era regressions lived outside that envelope. It found a real
267
+ that project's cwd. Every other lane runs _in this repo, from this repo, with one dev
268
+ server_, and all four rc.1-era regressions lived outside that envelope. It found a real
26
269
  bug on its first run.
27
270
  - **A failed "save to source" now says why** (#34). The endpoint answers 501 with an
28
271
  actionable sentence ("editableSources is not enabled…") and the client threw it away
@@ -30,8 +273,7 @@ introduced by rc.1 itself.
30
273
  broken one and the edit appeared to vanish. The server's reason is surfaced, and every
31
274
  failure path now states that the edit is still in the editor.
32
275
 
33
-
34
- ## 1.9.0-rc.1
276
+ ### 1.9.0-rc.1 (prerelease detail)
35
277
 
36
278
  Release candidate. Everything the nine-lens review and the first adopters raised is
37
279
  addressed; no features are pending. Install with `tosijs-ui@rc`.
@@ -39,10 +281,10 @@ addressed; no features are pending. Install with `tosijs-ui@rc`.
39
281
  **Edit and view are now separate hostnames**, which makes the security posture legible
40
282
  without reading config:
41
283
 
42
- | host | what it is | gate |
43
- | --- | --- | --- |
44
- | `<project>.dev.example.com` | read-only static preview — shareable | invite cookie |
45
- | `<project>.edit.dev.example.com` | live editable workspace — yours | session, always |
284
+ | host | what it is | gate |
285
+ | -------------------------------- | ------------------------------------ | --------------- |
286
+ | `<project>.dev.example.com` | read-only static preview — shareable | invite cookie |
287
+ | `<project>.edit.dev.example.com` | live editable workspace — yours | session, always |
46
288
 
47
289
  - **`tunnel.requireToken` now defaults to `true`.** The old default (read-open) was
48
290
  justified by "an expired link should degrade to a readable page when you open a second
@@ -53,10 +295,10 @@ without reading config:
53
295
  deliberately if you want a live read-only audience.
54
296
  - **An edit host with no tunnel behind it explains itself** — a 503 saying the workspace
55
297
  is offline, with a link to the static preview, instead of a bare 502. It deliberately
56
- does *not* fall back to serving the snapshot: you would think you were looking at live
298
+ does _not_ fall back to serving the snapshot: you would think you were looking at live
57
299
  work when you were not.
58
300
  - **A spent invite link says so.** The likely culprit is a chat-app link-preview bot,
59
- whose GET *is* the first use; previously you got a silent redirect and discovered it
301
+ whose GET _is_ the first use; previously you got a silent redirect and discovered it
60
302
  when a save failed.
61
303
 
62
304
  **Security fixes** (all found by review or adopters, none reported in the wild):
@@ -67,14 +309,14 @@ without reading config:
67
309
  - Build-error text (absolute paths from your machine) was injected into every served page
68
310
  with no auth check. The label stays public; the detail needs a session.
69
311
  - `rsync --delete` accepted any absolute path two segments deep — including `/usr/lib`
70
- and `/etc/caddy`, which it would have *mirrored*, i.e. emptied. Now an allowlist of
312
+ and `/etc/caddy`, which it would have _mirrored_, i.e. emptied. Now an allowlist of
71
313
  preview roots, extracted and tested.
72
314
 
73
315
  **Adopter-reported fixes** (all from tosijs-3d, all consumer-facing):
74
316
 
75
317
  - **`tosijs-make-icons` stripped `fill-rule`** (#30), so a compound path using `evenodd`
76
318
  for holes — a keyboard drawn as one path — rendered the holes solid. `fill-rule` is
77
- fill *topology*, not colour, and takes no part in the tinting the strip exists for.
319
+ fill _topology_, not colour, and takes no part in the tinting the strip exists for.
78
320
  It turned out to be stripped in **three** places, not one; the `stroked/` branch is the
79
321
  one that bit. This ships as a bin, so it affects anyone generating their own icon data.
80
322
  - **The doc-site hydrate bundle was written into the library `dist/`** (#31) and shipped
@@ -92,7 +334,7 @@ without reading config:
92
334
  **Correctness:**
93
335
 
94
336
  - **A 403 could destroy uncommitted work.** An unauthorized read fell through to GitHub
95
- `main`, so you edited the *published* file, and the save then handed you a download of
337
+ `main`, so you edited the _published_ file, and the save then handed you a download of
96
338
  it — applying which silently reverts your working copy. 401/403 is now its own case in
97
339
  both editors.
98
340
  - **The last-good build protection moved to where the wipe is.** It lived in the dev
@@ -104,8 +346,7 @@ without reading config:
104
346
  - `?t=` is only intercepted when a tunnel is configured, and only on GET — `t` is the
105
347
  classic cache-buster name, and a 302'd POST loses its body.
106
348
 
107
-
108
- ## 1.9.0-beta.3
349
+ ### 1.9.0-beta.3 (prerelease detail)
109
350
 
110
351
  Fixes three blockers found by the nine-lens review of beta.2, all in the remote-editing
111
352
  feature, plus a rebuild loop the review did not catch because nothing automated runs
@@ -113,20 +354,19 @@ feature, plus a rebuild loop the review did not catch because nothing automated
113
354
 
114
355
  - **`bun run tunnel --link` never worked.** It ran `pgrep -f 'bun bin/dev.ts'`, which does
115
356
  not match the documented start command (`bun --watch bin/dev.ts`) — so the headline flow
116
- was unreachable. Worse, it *did* match `bun bin/dev.ts --build-only`, whose process exits
357
+ was unreachable. Worse, it _did_ match `bun bin/dev.ts --build-only`, whose process exits
117
358
  before the signal handler is registered, so the default SIGUSR2 disposition **killed
118
359
  in-flight builds**. Now it asks the dev server over a loopback-only `/__devlink` endpoint:
119
360
  no process guessing, no signals, and it cannot touch a sibling project. (`/__devlink` is
120
361
  refused over the tunnel — a read-only visitor minting a write session would be privilege
121
362
  escalation.)
122
- - **Write authorization failed OPEN.** It inferred "this is local" from the *absence* of
363
+ - **Write authorization failed OPEN.** It inferred "this is local" from the _absence_ of
123
364
  `X-Forwarded-*`, so any forwarder that doesn't set them — `ssh -R` with `GatewayPorts yes`,
124
365
  `ngrok tcp`, `socat`, iptables DNAT, nginx without `forwardfor` — delivered an off-machine
125
366
  request as `{peer: 127.0.0.1, no headers}` and got an **arbitrary repo write**, which the
126
367
  watcher rebuilds and runs. The dev server now binds a **separate loopback-only port** for
127
368
  tunnel traffic; arriving there always requires a session. Which socket you connected to is
128
- not something a client can forge. `bun run tunnel` also verifies the remote port is bound
129
- to loopback and says so loudly if it isn't.
369
+ not something a client can forge.
130
370
  - **The decision guarding that RCE had no tests**, while its own comment claimed a
131
371
  regression test that did not exist. It is now `mayWriteSource()` — pure, exported, and
132
372
  covered including a named regression case for the fail-open above.
@@ -140,13 +380,13 @@ feature, plus a rebuild loop the review did not catch because nothing automated
140
380
  workspace, so silently adopting a neighbour's config would publish the wrong project.
141
381
  - **Fix: the tunnel probed the wrong port**, so forwarding to a dead listener failed
142
382
  silently with a 502 and a healthy-looking dev server (tosijs-ui#28). `ssh -R` can't
143
- catch it — the *remote* bind succeeds, so `ExitOnForwardFailure` stays quiet; it's the
383
+ catch it — the _remote_ bind succeeds, so `ExitOnForwardFailure` stays quiet; it's the
144
384
  local end that connects to nothing. It now probes the port it actually forwards to and
145
385
  **refuses to open**, naming the likely cause (including "your tosijs-ui predates the
146
386
  tunnel listener").
147
387
  - **Tunnelled workspaces self-register too** (tosijs-ui#29). Static previews have
148
388
  self-registered since beta.1, but a tunnelled host still needed a block hand-added to
149
- the *shared* Caddyfile — so the one remaining manual edit was the **riskier** one: a
389
+ the _shared_ Caddyfile — so the one remaining manual edit was the **riskier** one: a
150
390
  typo there breaks routing for every project on the box, which is exactly what the
151
391
  fragment design set out to abolish. `tosijs-tunnel` now writes its own fragment with
152
392
  the same validate-before-reload discipline, and the shared file names no hostnames at
@@ -162,13 +402,12 @@ feature, plus a rebuild loop the review did not catch because nothing automated
162
402
  watcher path and declared an **optional peer**, so building needs nothing extra and the
163
403
  watching dev server explains itself if it is missing.
164
404
  - **Fix: `bun start` was in a rebuild loop** (measured at 899 restarts in ~40s), which made
165
- the documented dev command unusable. The build stamp imported the *generated*
405
+ the documented dev command unusable. The build stamp imported the _generated_
166
406
  `src/version.ts`, putting it in `bun --watch`'s module graph while prebuild rewrote it every
167
407
  build. The import is gone, and generated files are now written **only when the content
168
408
  changes**, which makes the whole class of self-write loops impossible.
169
409
 
170
-
171
- ## 1.9.0-beta.2
410
+ ### 1.9.0-beta.2 (prerelease detail)
172
411
 
173
412
  > Prerelease — install with `tosijs-ui@beta`. `latest` stays on 1.8.0.
174
413
 
@@ -182,7 +421,7 @@ same watcher, same files.
182
421
  which is what lets one small VPS serve many projects. The work stays where the data is.
183
422
  - **Magic-link auth.** Two tokens on purpose: the **link** rides in a URL so it is the
184
423
  one that leaks (history, `Referer`, proxy logs, chat-app link previews) and is
185
- therefore *single-use and 15 minutes*; the **session** never appears in a URL, arrives
424
+ therefore _single-use and 15 minutes_; the **session** never appears in a URL, arrives
186
425
  as an `HttpOnly` cookie, and is durable. A durable token pasted into a URL would be
187
426
  strictly worse than basic auth. `SameSite=Lax` gives the write endpoint free CSRF
188
427
  protection.
@@ -205,8 +444,7 @@ same watcher, same files.
205
444
  session) — Caddy serving static files has no session store — but equivalent to the
206
445
  basicauth it replaces, with no dialog.
207
446
 
208
-
209
- ## 1.9.0-beta.1
447
+ ### 1.9.0-beta.1 (prerelease detail)
210
448
 
211
449
  > **Prerelease.** The preview-host system works end to end and is in daily use, but the
212
450
  > plan it belongs to (`REMOTE-ACCESS-PLAN.md`) has two more phases. Published under the
@@ -284,7 +522,7 @@ same watcher, same files.
284
522
  shown), plus an **advisories-per-package tally** — a dependency that keeps producing them
285
523
  is a code smell worth replacing rather than patching.
286
524
  - **Fix (issue #24, reported by a consumer): nav `order` now sorts numerically.** Section nav
287
- and the generated `<!-- toc -->` built a zero-padded *string* key and compared it lexically,
525
+ and the generated `<!-- toc -->` built a zero-padded _string_ key and compared it lexically,
288
526
  so a fractional order silently sorted to the **end** of its section — `order: 1.5` landed
289
527
  after `2`, `3`, … because `"01.5" > "0002"` as text. "Insert a page at N.5" reads like a
290
528
  supported idiom and did the opposite with no warning. The same padding capped `order` at 4
@@ -299,7 +537,7 @@ same watcher, same files.
299
537
  above was effectively unfindable. The comparator needs no delimiter; the NUL is gone.
300
538
  - **Adopting the gate — budget for one upgrade, not one pin.** From the first real
301
539
  adoption (`tosijs`, which went from 12 blocking advisories to zero): if the gate flags
302
- **`brace-expansion`**, note that `GHSA-mh99-v99m-4gvg` marks *every* version below
540
+ **`brace-expansion`**, note that `GHSA-mh99-v99m-4gvg` marks _every_ version below
303
541
  **5.0.8** affected, so the override is mandatory — and `brace-expansion@5` breaks
304
542
  **eslint 8**'s bundled `minimatch@3` with `expand is not a function`. In practice that
305
543
  means **an eslint 8 → 10 migration** (flat config + typescript-eslint 8), not a one-line
@@ -360,13 +598,13 @@ same watcher, same files.
360
598
  it never sits over the code editors in side-by-side mode). The collapsed handle carries the
361
599
  example's pass/fail color, "run tests" is a checkbox that greys when off, and the bar stays
362
600
  visible when the example is maximized.
363
- - **Build system is fragility-tolerant.** An *unsupported import* in a live example (a bare
601
+ - **Build system is fragility-tolerant.** An _unsupported import_ in a live example (a bare
364
602
  specifier the runtime can't resolve) now downgrades that block to display-only with a warning
365
603
  instead of failing the whole build; genuine syntax errors still fail the build.
366
604
  - **Removed a render-skip anti-pattern** from `<tosi-segmented>`, `<tosi-color>`, and
367
- `<tosi-form>`. They previously depended on `render()` being *skipped* after interaction via a
605
+ `<tosi-form>`. They previously depended on `render()` being _skipped_ after interaction via a
368
606
  `valueChanged` flag — fragile timing that broke under tosijs's parts changes. Renders are now
369
- idempotent: they skip only *provably-redundant* writes by comparing values (colors compared
607
+ idempotent: they skip only _provably-redundant_ writes by comparing values (colors compared
370
608
  **parsed**, so a `#rrggbb ↔ rgba()` round-trip no longer clobbers the caret/selection in the
371
609
  field being edited).
372
610
  - **Code editor background is now a neutral off-white** (off-black in dark mode) instead of a
@@ -391,7 +629,7 @@ same watcher, same files.
391
629
  build was locked to one mount — adding a custom domain to a GitHub project page flipped the
392
630
  live site to the root while `docs/` still pointed at `/repo`, 404ing every asset into a bare,
393
631
  unstyled shell with no warning. Functional URLs are now emitted **relative to each page**, so
394
- one build works at `/repo`, a custom-domain root, or a moved mount with no rebuild. *Metadata*
632
+ one build works at `/repo`, a custom-domain root, or a moved mount with no rebuild. _Metadata_
395
633
  URLs (`canonical`, `og:url`, `og:image`, `sitemap.xml`) stay absolute via `baseUrl`+`basePath`
396
634
  — so `basePath` now only affects metadata, never the page's assets. (The hydrated SPA's own
397
635
  nav/`pushState` and `__TJS_LOCAL_BASE` remain root-relative — tracked in #16.)
@@ -424,7 +662,7 @@ same watcher, same files.
424
662
  - **Fix: "edit page source" showed HTML instead of source in a dev server without
425
663
  `editableSources`.** The `/__docstore/source` endpoint was only routed when
426
664
  `editableSources` was on; otherwise the request fell through to the SPA `index.html`
427
- fallback and returned the rendered page at status 200 — so the client loaded HTML *as* the
665
+ fallback and returned the rendered page at status 200 — so the client loaded HTML _as_ the
428
666
  source (and save read HTML). The endpoint is now handled unconditionally and answers a clean
429
667
  `501` when editing is disabled, so `loadSource` falls back to the GitHub raw source (read-only
430
668
  editing, matching the deployed site). Belt-and-suspenders: the client also rejects any
@@ -432,8 +670,8 @@ same watcher, same files.
432
670
  more fully in `doc-site-system.md`.
433
671
  - **Local example save now gives feedback.** "Save changes (local)" writes a per-browser
434
672
  scratchpad (not the file) — it used to do so silently, reading as a no-op. It now posts a
435
- toast (`postNotification`): *"Saved to this browser only — use 'Save to source' to write the
436
- file."*
673
+ toast (`postNotification`): _"Saved to this browser only — use 'Save to source' to write the
674
+ file."_
437
675
 
438
676
  - **`valueRenderer(type)` + declarative `<tosi-table>` column `type`.** A new exported helper
439
677
  turns a compact type string into a reusable, locale-aware renderer with a sensible default
@@ -449,7 +687,7 @@ same watcher, same files.
449
687
 
450
688
  - **Deprecation cleanup: `on<Event>` component callbacks → `handle<Event>`.** tosijs reserves the
451
689
  `on<Event>` prefix for elements-factory event-handler sugar and now warns when a component
452
- *defines* such a property. Renamed the internal ones: `onResize` → `handleResize` (`size-break`,
690
+ _defines_ such a property. Renamed the internal ones: `onResize` → `handleResize` (`size-break`,
453
691
  `side-nav`, `code-editor`, `babylon-3d`, `tab-selector`) and `onScrollEnd` → `handleScrollEnd`
454
692
  (`data-table`). Verified: those pages now load with zero `on<Event>` deprecation warnings.
455
693
  (`<tosi-tabs>`'s public `onCloseTab` still uses the old name — a breaking rename deferred to a
@@ -517,7 +755,7 @@ the real transpiler + editor on demand; saving keeps the transpiled JS.
517
755
  With `importResolver` enabled, a live example can `import` any npm package — resolved through a
518
756
  same-origin service worker (tjs-lang's import-resolver). Three execution modes are flaggable on
519
757
  the fence: `inline` (default), `iframe` (DOM/CSS isolation), `ide` (recognized; iframe path for
520
- now). Fence grammar is order-free: `` ```ts:ide#demo `` and `` ```ts#demo:ide `` both parse.
758
+ now). Fence grammar is order-free: ` ```ts:ide#demo ` and ` ```ts#demo:ide ` both parse.
521
759
 
522
760
  ### Doc-system & build
523
761