Use Jerboa v0.2.8 toolchain

ober

1bfb3edbfccf6323943f66456f37a1638cfb2aca

diff --git a/.build.yml b/.build.yml
index 8df3795..945e1bb 100644
--- a/.build.yml
+++ b/.build.yml
@@ -25,8 +25,8 @@ environment:
   LATEST_SITE: lisp.srht.site
   LATEST_SUBDIR: /jerboa-browser/latest
   LATEST_TARBALL: jerboa-browser-linux-amd64.tar.gz
-  JERBOA_COMMIT: 73176e945f656334b8b1cf0d6b11c6c41f8ba427
-  JERBOA_TREE: 5e2ce0ac497d4b93d3a5859a13856fdbb0987ba3
+  JERBOA_COMMIT: ae716feb376ce9adefcad65e923ebfd866131947
+  JERBOA_TREE: 9fafa4b48f6a4d1c166dc3ae57be720237ee988b
 sources:
   - https://git.sr.ht/~lisp/jerboa-browser
 artifacts:
diff --git a/AGENTS.md b/AGENTS.md
index 0f5b749..9b29031 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -371,7 +371,6 @@ improved versions of the above.
 When working in a Jerboa project, **ONLY modify files in the current repo** unless the user explicitly names another path.
 
 Common sibling repos that exist but must NOT be touched without explicit instruction:
-- `~/mine/jerboa-emacs` — **NEVER touch**. Another model owns it.
 - `~/mine/jerboa-mcp` — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told.
 - `~/mine/jerboa-shell` — Only modify when user explicitly says to work there.
 - `~/mine/gerbil-mcp` — **NEVER touch**. Deprecated.
diff --git a/Dockerfile.qt b/Dockerfile.qt
index 98022e6..7a6c5e2 100644
--- a/Dockerfile.qt
+++ b/Dockerfile.qt
@@ -15,9 +15,11 @@
 ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
 FROM ${BASE_IMAGE} AS builder
 
-ARG JERBOA_VERSION=master
-ARG JERBOA_SOURCE_COMMIT=70c14e8e0e0d9f194bbd142e7418d2b546c9c468
-ARG JERBOA_SOURCE_TREE=4e5dbcc94684a7e61187897fa484a54f77c78929
+ARG JERBOA_VERSION=v0.2.8
+ARG JERBOA_SOURCE_URL=https://git.sr.ht/~lisp/jerboa
+ARG JERBOA_SOURCE_COMMIT=ae716feb376ce9adefcad65e923ebfd866131947
+ARG JERBOA_SOURCE_TREE=9fafa4b48f6a4d1c166dc3ae57be720237ee988b
+ARG JERBOA_SOURCE_NATIVE_FEATURES=tls,crypto,postgres_feat,wasm
 ARG JBROWSER_SOURCE_COMMIT
 ARG JBROWSER_SOURCE_TREE
 ARG APT_SNAPSHOT=20260701T000000Z
@@ -59,7 +61,9 @@ RUN sh /tmp/ensure-rust.sh && \
 
 COPY support/ensure-jerboa.sh support/jerboa-release-sha256.txt /tmp/
 RUN JERBOA_SOURCE_COMMIT="$JERBOA_SOURCE_COMMIT" \
+    JERBOA_SOURCE_URL="$JERBOA_SOURCE_URL" \
     JERBOA_SOURCE_TREE="$JERBOA_SOURCE_TREE" \
+    JERBOA_NATIVE_FEATURES="$JERBOA_SOURCE_NATIVE_FEATURES" \
     sh /tmp/ensure-jerboa.sh "$JERBOA_VERSION" /usr/local/bin && \
     command -v jerbuild && \
     jerbuild --version && \
diff --git a/Makefile b/Makefile
index 1449775..28fc10e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,12 @@
 # jerbuild bundles Chez Scheme + the jerboa stdlib, so building the browser
 # entry needs only `jerbuild` + a C compiler. CI uses a pinned project-local
 # release toolchain; developer machines can still override JERBUILD explicitly.
-JERBOA_VERSION ?= master
-JERBOA_COMMIT ?= 70c14e8e0e0d9f194bbd142e7418d2b546c9c468
-JERBOA_TREE ?= 4e5dbcc94684a7e61187897fa484a54f77c78929
-JERBOA_BUNDLE_SHA256 ?= ac8587f3b466f7d6d3047b4cafefd9985a08983503a5ad0764d4947cef25e828
+JERBOA_VERSION ?= v0.2.8
+JERBOA_SOURCE_URL ?= https://git.sr.ht/~lisp/jerboa
+JERBOA_COMMIT ?= ae716feb376ce9adefcad65e923ebfd866131947
+JERBOA_TREE ?= 9fafa4b48f6a4d1c166dc3ae57be720237ee988b
+JERBOA_SOURCE_NATIVE_FEATURES ?= tls,crypto,postgres_feat,wasm
+JERBOA_BUNDLE_SHA256 ?= 6e9aae5475dff96e70f4d9710d47d37fa04eee8948775439f2d796ffb7e2ae8c
 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
 JERBUILD ?= $(JERBOA_TOOL_DIR)/jerbuild
 # Recursive on purpose: check-jerbuild may install the pinned local tool before
@@ -132,7 +134,7 @@ all: binary
 check-jerbuild:
 	@if [ "$(JERBUILD)" = "$(JERBOA_TOOL_DIR)/jerbuild" ] && \
 	   { [ ! -x "$(JERBUILD)" ] || [ "$$(cat "$(JERBOA_TOOL_DIR)/.jerboa-version" 2>/dev/null || true)" != "$(JERBOA_VERSION)" ]; }; then \
-	  JERBOA_SOURCE_COMMIT="$(JERBOA_COMMIT)" JERBOA_SOURCE_TREE="$(JERBOA_TREE)" \
+	  JERBOA_SOURCE_URL="$(JERBOA_SOURCE_URL)" JERBOA_SOURCE_COMMIT="$(JERBOA_COMMIT)" JERBOA_SOURCE_TREE="$(JERBOA_TREE)" JERBOA_NATIVE_FEATURES="$(JERBOA_SOURCE_NATIVE_FEATURES)" \
 	    sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \
 	fi
 	@"$(JERBUILD)" --jerboa-home >/dev/null 2>&1 || { echo $(JERBUILD_ERROR) >&2; exit 1; }
@@ -148,7 +150,7 @@ ensure-jerboa-tools:
 	  echo "=== Using explicit Jerboa toolchain: $(JERBUILD) ==="; \
 	else \
 	  echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \
-	  JERBOA_SOURCE_COMMIT="$(JERBOA_COMMIT)" JERBOA_SOURCE_TREE="$(JERBOA_TREE)" \
+	  JERBOA_SOURCE_URL="$(JERBOA_SOURCE_URL)" JERBOA_SOURCE_COMMIT="$(JERBOA_COMMIT)" JERBOA_SOURCE_TREE="$(JERBOA_TREE)" JERBOA_NATIVE_FEATURES="$(JERBOA_SOURCE_NATIVE_FEATURES)" \
 	    sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \
 	fi
 	@"$(JERBUILD)" --jerboa-home >/dev/null || { \
@@ -419,8 +421,10 @@ podman-static-qt: check-podman
 	$(PODMAN) build $(PODMAN_BUILD_FLAGS) --platform $(PODMAN_PLATFORM) \
 	  --build-arg BASE_IMAGE="$(STATIC_QT_BASE_IMAGE)" \
 	  --build-arg JERBOA_VERSION="$(JERBOA_VERSION)" \
+	  --build-arg JERBOA_SOURCE_URL="$(JERBOA_SOURCE_URL)" \
 	  --build-arg JERBOA_SOURCE_COMMIT="$(JERBOA_COMMIT)" \
 	  --build-arg JERBOA_SOURCE_TREE="$(JERBOA_TREE)" \
+	  --build-arg JERBOA_SOURCE_NATIVE_FEATURES="$(JERBOA_SOURCE_NATIVE_FEATURES)" \
 	  --build-arg APT_SNAPSHOT="$(STATIC_QT_APT_SNAPSHOT)" \
 	  --build-arg JBROWSER_SOURCE_COMMIT="$$(git rev-parse HEAD)" \
 	  --build-arg JBROWSER_SOURCE_TREE="$$(git rev-parse 'HEAD^{tree}')" \
diff --git a/docs/credential-handling.md b/docs/credential-handling.md
index 09bbffe..37f32c9 100644
--- a/docs/credential-handling.md
+++ b/docs/credential-handling.md
@@ -31,9 +31,9 @@ PIV-wrapped data-key format used for bookmarks. When disabled, history commands
 report that history is disabled and no history entries are recorded.
 
 Encrypted bookmark and history writes go through `(std os secure-output)`.
-The pinned v0.2.7 toolchain ships the upstream module; the browser keeps the
+The pinned v0.2.8 toolchain ships the upstream module; the browser keeps the
 same fail-closed write path when release archives are unavailable by building
-that exact v0.2.7 source tree into the project-local `.jerboa/bin` toolchain.
+that exact v0.2.8 source tree into the project-local `.jerboa/bin` toolchain.
 
 ## Browser Profile Vault
 
diff --git a/scheme/browser-adblock-test.ss b/scheme/browser-adblock-test.ss
index 6febddd..dc4edd6 100644
--- a/scheme/browser-adblock-test.ss
+++ b/scheme/browser-adblock-test.ss
@@ -14,7 +14,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-adblock
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-buffers-test.ss b/scheme/browser-buffers-test.ss
index 3436d54..7494443 100644
--- a/scheme/browser-buffers-test.ss
+++ b/scheme/browser-buffers-test.ss
@@ -14,7 +14,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-buffers
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers))
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers))
 
 ;;; ─── tiny test framework (shared shape with browser-gui-test.ss) ──────────
 (define *pass* 0)
diff --git a/scheme/browser-commands-test.ss b/scheme/browser-commands-test.ss
index 9c1ad3a..ed9ea29 100644
--- a/scheme/browser-commands-test.ss
+++ b/scheme/browser-commands-test.ss
@@ -16,7 +16,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-commands
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-fuzz-test.ss b/scheme/browser-fuzz-test.ss
index 74f99be..63e8ebf 100644
--- a/scheme/browser-fuzz-test.ss
+++ b/scheme/browser-fuzz-test.ss
@@ -5,7 +5,7 @@
 ;;; runs enough iterations to catch parser exceptions, timeout loops, and obvious
 ;;; UTF-8/embedded-control regressions across the Scheme -> C ABI boundary.
 
-(import (scheme)
+(import (jerboa prelude unsafe) (scheme)
         (browser)
         (browser keymap)
         (std test fuzz))
diff --git a/scheme/browser-gui-test.ss b/scheme/browser-gui-test.ss
index 64ce29d..6574862 100644
--- a/scheme/browser-gui-test.ss
+++ b/scheme/browser-gui-test.ss
@@ -17,7 +17,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-gui
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser))
+(import (jerboa prelude unsafe) (scheme) (browser))
 
 ;;; ─── tiny test framework (ported from jerboa-emacs test-qt.ss) ────────────
 (define *pass* 0)
diff --git a/scheme/browser-hint-test.ss b/scheme/browser-hint-test.ss
index 4235136..1bd32d0 100644
--- a/scheme/browser-hint-test.ss
+++ b/scheme/browser-hint-test.ss
@@ -11,7 +11,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-hint
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-keymap-test.ss b/scheme/browser-keymap-test.ss
index b21e68d..22578bc 100644
--- a/scheme/browser-keymap-test.ss
+++ b/scheme/browser-keymap-test.ss
@@ -9,7 +9,7 @@
 ;;; Run:  make test-keymap     (or: scheme --script … with JERBOA_HOME set)
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser keymap))
+(import (jerboa prelude unsafe) (scheme) (browser keymap))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
 (define *pass* 0)
diff --git a/scheme/browser-keys-test.ss b/scheme/browser-keys-test.ss
index 77e38ad..135c37a 100644
--- a/scheme/browser-keys-test.ss
+++ b/scheme/browser-keys-test.ss
@@ -14,7 +14,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-keys
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-minibuffer-test.ss b/scheme/browser-minibuffer-test.ss
index b10aea3..ab7628d 100644
--- a/scheme/browser-minibuffer-test.ss
+++ b/scheme/browser-minibuffer-test.ss
@@ -5,7 +5,7 @@
 ;;; Run:  make test-minibuffer
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser fuzzy) (browser minibuffer))
+(import (jerboa prelude unsafe) (scheme) (browser fuzzy) (browser minibuffer))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
 (define *pass* 0)
diff --git a/scheme/browser-nav-test.ss b/scheme/browser-nav-test.ss
index d95ae06..4fa4c7e 100644
--- a/scheme/browser-nav-test.ss
+++ b/scheme/browser-nav-test.ss
@@ -10,7 +10,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-nav
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-pass-test.ss b/scheme/browser-pass-test.ss
index 44e2516..c9bc0c5 100644
--- a/scheme/browser-pass-test.ss
+++ b/scheme/browser-pass-test.ss
@@ -14,7 +14,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-pass
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer) (browser passwords) (browser commands))
 
 ;;; ─── tiny test framework (shared shape with the other test files) ─────────
diff --git a/scheme/browser-polish-test.ss b/scheme/browser-polish-test.ss
index 246518c..0665f21 100644
--- a/scheme/browser-polish-test.ss
+++ b/scheme/browser-polish-test.ss
@@ -9,7 +9,7 @@
 ;;; Run:  QT_QPA_PLATFORM=offscreen make test-polish
 ;;; Exits 0 on success, 1 on any failed case.
 
-(import (scheme) (browser) (browser buffers)
+(import (jerboa prelude unsafe) (scheme) (browser) (browser buffers)
         (browser keymap) (browser minibuffer)
         (browser bookmarks) (browser commands)
         (browser observability))
diff --git a/scheme/browser-securestore-test.ss b/scheme/browser-securestore-test.ss
index 63085a6..bbb0fd5 100644
--- a/scheme/browser-securestore-test.ss
+++ b/scheme/browser-securestore-test.ss
@@ -7,7 +7,8 @@
 ;;; `make test` (and `make test-securestore`); needs ./lib/libjerboa_native.*
 ;;; reachable for the Rust crypto backend (a symlink in lib/ provides it).
 
-(import (except (scheme)
+(import (jerboa prelude unsafe)
+        (except (scheme)
                 make-hash-table hash-table? sort sort! printf fprintf
                 path-extension path-absolute? with-input-from-string
                 with-output-to-string iota 1+ 1- partition make-date make-time)
diff --git a/scheme/browser-test.ss b/scheme/browser-test.ss
index b500b70..25b16bc 100644
--- a/scheme/browser-test.ss
+++ b/scheme/browser-test.ss
@@ -11,7 +11,7 @@
 ;;; (or via the wrapper that sets JERBOA_BROWSER_LIB / libdirs for you).
 ;;; Exits 0 on success, 1 on any failed check.
 
-(import (scheme) (browser))
+(import (jerboa prelude unsafe) (scheme) (browser))
 
 (define passed 0)
 (define failed 0)
diff --git a/scheme/browser.ss b/scheme/browser.ss
index fdbb1c9..76d3d44 100644
--- a/scheme/browser.ss
+++ b/scheme/browser.ss
@@ -82,6 +82,13 @@
     browser-downloads-snapshot
     browser-download-count
     browser-download-cancel!
+    ;; secure-memory bridge for repo-local std/crypto/secure-mem shim
+    browser-secure-alloc
+    browser-secure-free
+    browser-secure-wipe
+    browser-secure-random-fill
+    browser-secure-region-store!
+    browser-secure-region-load!
     ;; window / buffer chrome
     browser-window-open
     browser-window-close
@@ -448,6 +455,12 @@
   (define-c-lambda %downloads-snapshot () char-string "jwb_downloads_snapshot")
   (define-c-lambda %download-count ()    int "jwb_download_count")
   (define-c-lambda %download-cancel (uint32) int "jwb_download_cancel")
+  (define-c-lambda %secure-alloc (size_t) uint64 "jwb_secure_alloc")
+  (define-c-lambda %secure-free (uint64 size_t) int "jwb_secure_free")
+  (define-c-lambda %secure-wipe (uint64 size_t) int "jwb_secure_wipe")
+  (define-c-lambda %secure-random-fill (uint64 size_t) int "jwb_secure_random_fill")
+  (define-c-lambda %secure-region-store! (uint64 u8* size_t) int "jwb_secure_region_store")
+  (define-c-lambda %secure-region-load! (u8* uint64 size_t) int "jwb_secure_region_load")
   ;; window / buffer chrome (browser_window.cpp)
   (define-c-lambda %win-open       ()                        uint64      "jwb_window_open")
   (define-c-lambda %win-close      (uint64)                  int         "jwb_window_close")
@@ -526,6 +539,24 @@
     (let ((st (%runtime-init)))
       (if (= st 0) (ok* #t) (err* (string-append "runtime init: " (%last-error))))))
 
+  (def (browser-secure-alloc size)
+    (%secure-alloc size))
+
+  (def (browser-secure-free ptr size)
+    (%secure-free ptr size))
+
+  (def (browser-secure-wipe ptr size)
+    (%secure-wipe ptr size))
+
+  (def (browser-secure-random-fill ptr size)
+    (%secure-random-fill ptr size))
+
+  (def (browser-secure-region-store! ptr bv len)
+    (%secure-region-store! ptr bv len))
+
+  (def (browser-secure-region-load! bv ptr len)
+    (%secure-region-load! bv ptr len))
+
   ;; --- context / view lifecycle ------------------------------------------
   (def (browser-open-context caps)
     (let ((h (%context-open caps)))
diff --git a/scheme/std/crypto/secure-mem.ss b/scheme/std/crypto/secure-mem.ss
index 0fdfca3..53fc2ba 100644
--- a/scheme/std/crypto/secure-mem.ss
+++ b/scheme/std/crypto/secure-mem.ss
@@ -1,17 +1,23 @@
 #!chezscheme
 ;;; (std crypto secure-mem) — repo-local compatibility shim.
 ;;;
-;;; The pinned Jerboa v0.2.4 stdlib hardcodes 4 KiB guard pages in its native
-;;; secure-memory allocator, which fails on macOS arm64 hosts with larger system
-;;; pages. This module preserves the same public API for this repo and delegates
-;;; to page-size-aware jwb_secure_* ABI functions in libjerboa_browser.
+;;; Jerboa v0.2.8 provides secure-bytevector APIs in this module. This repo keeps
+;;; a page-size-aware bridge to libjerboa_browser for macOS arm64 hosts, where
+;;; older fixed-page secure-memory allocation failed, and mirrors the v0.2.8 API
+;;; on top of that bridge.
 
 (library (std crypto secure-mem)
   (export
     secure-alloc secure-free secure-wipe
     secure-random-fill
     with-secure-region
-    secure-region? secure-region-pointer secure-region-size)
+    secure-region? secure-region-pointer secure-region-size
+    make-secure-bytevector bytevector->secure-bytevector
+    secure-bytevector? secure-bytevector-length
+    secure-bytevector-ref secure-bytevector-set!
+    secure-bytevector->bytevector secure-bytevector-wipe!
+    secure-bytevector-random-fill! secure-bytevector-free
+    with-secure-bytevector)
 
   (import (except (scheme)
                   make-hash-table hash-table?
@@ -26,14 +32,41 @@
           (browser))
 
   (defstruct secure-region (pointer size))
+  (defstruct %secure-bytevector (region length))
+  (def secure-bytevector? %secure-bytevector?)
+  (def %make-secure-bytevector make-%secure-bytevector)
+  (def secure-bytevector-region %secure-bytevector-region)
+  (def secure-bytevector-length %secure-bytevector-length)
 
-  (define-c-lambda %secure-alloc (size_t) uint64 "jwb_secure_alloc")
-  (define-c-lambda %secure-free (uint64 size_t) int "jwb_secure_free")
-  (define-c-lambda %secure-wipe (uint64 size_t) int "jwb_secure_wipe")
-  (define-c-lambda %secure-random-fill (uint64 size_t) int "jwb_secure_random_fill")
+  (def (checked-size who size)
+    (unless (and (integer? size) (>= size 0))
+      (error who "size must be a non-negative integer" size))
+    size)
+
+  (def (checked-byte who byte)
+    (unless (and (integer? byte) (>= byte 0) (<= byte 255))
+      (error who "byte must be an integer in [0,255]" byte))
+    byte)
+
+  (def (wipe-heap-bytevector! bv)
+    (when (bytevector? bv)
+      (let ((n (bytevector-length bv)))
+        (let loop ((i 0))
+          (when (< i n)
+            (bytevector-u8-set! bv i 0)
+            (loop (+ i 1)))))))
+
+  (def (checked-index who sbv index)
+    (let ((len (secure-bytevector-length sbv)))
+      (unless (and (integer? index) (>= index 0) (< index len))
+        (error who "index out of bounds" index len))
+      index))
 
   (def (secure-alloc size)
-    (let ((ptr (%secure-alloc size)))
+    (checked-size 'secure-alloc size)
+    (when (= size 0)
+      (error 'secure-alloc "size must be greater than zero" size))
+    (let ((ptr (browser-secure-alloc size)))
       (when (= ptr 0)
         (error 'secure-alloc "secure allocation failed" size))
       (make-secure-region ptr size)))
@@ -42,7 +75,7 @@
     (let ((ptr (secure-region-pointer region))
           (size (secure-region-size region)))
       (unless (= ptr 0)
-        (unless (= 1 (%secure-free ptr size))
+        (unless (= 1 (browser-secure-free ptr size))
           (error 'secure-free "secure free failed"))
         (secure-region-pointer-set! region 0)
         (secure-region-size-set! region 0)))
@@ -52,7 +85,7 @@
     (let ((ptr (secure-region-pointer region)))
       (when (= ptr 0)
         (error 'secure-wipe "secure region has been freed"))
-      (unless (= 1 (%secure-wipe ptr (secure-region-size region)))
+      (unless (= 1 (browser-secure-wipe ptr (secure-region-size region)))
         (error 'secure-wipe "secure wipe failed")))
     (void))
 
@@ -60,10 +93,101 @@
     (let ((ptr (secure-region-pointer region)))
       (when (= ptr 0)
         (error 'secure-random-fill "secure region has been freed"))
-      (unless (= 1 (%secure-random-fill ptr (secure-region-size region)))
+      (unless (= 1 (browser-secure-random-fill ptr (secure-region-size region)))
         (error 'secure-random-fill "CSPRNG failed")))
     (void))
 
+  (def (secure-bytevector-allocation-size len)
+    (if (= len 0) 1 len))
+
+  (def (live-region who sbv)
+    (unless (secure-bytevector? sbv)
+      (error who "expected secure-bytevector" sbv))
+    (let ((region (secure-bytevector-region sbv)))
+      (when (= (secure-region-pointer region) 0)
+        (error who "secure bytevector has been freed"))
+      region))
+
+  (def (secure-bytevector-load who sbv)
+    (let* ((region (live-region who sbv))
+           (len (secure-bytevector-length sbv))
+           (out (make-bytevector len 0)))
+      (unless (= 1 (browser-secure-region-load! out (secure-region-pointer region) len))
+        (error who "could not copy bytes from secure memory"))
+      out))
+
+  (def (secure-bytevector-store! who sbv bv)
+    (let ((region (live-region who sbv))
+          (len (bytevector-length bv)))
+      (unless (= len (secure-bytevector-length sbv))
+        (error who "bytevector length mismatch" len (secure-bytevector-length sbv)))
+      (unless (= 1 (browser-secure-region-store! (secure-region-pointer region) bv len))
+        (error who "could not copy bytes into secure memory")))
+    (void))
+
+  (def (make-secure-bytevector size . fill-opt)
+    (let* ((len (checked-size 'make-secure-bytevector size))
+           (fill (and (pair? fill-opt)
+                      (checked-byte 'make-secure-bytevector (car fill-opt))))
+           (region (secure-alloc (secure-bytevector-allocation-size len)))
+           (sbv (%make-secure-bytevector region len)))
+      (when fill
+        (let ((tmp (make-bytevector len fill)))
+          (dynamic-wind
+            (lambda () #f)
+            (lambda ()
+              (secure-bytevector-store! 'make-secure-bytevector sbv tmp))
+            (lambda ()
+              (wipe-heap-bytevector! tmp)))))
+      sbv))
+
+  (def (bytevector->secure-bytevector bv)
+    (unless (bytevector? bv)
+      (error 'bytevector->secure-bytevector "expected bytevector" bv))
+    (let* ((len (bytevector-length bv))
+           (region (secure-alloc (secure-bytevector-allocation-size len)))
+           (sbv (%make-secure-bytevector region len)))
+      (if (= 1 (browser-secure-region-store! (secure-region-pointer region) bv len))
+          sbv
+          (begin
+            (secure-free region)
+            (error 'bytevector->secure-bytevector "could not copy bytes into secure memory")))))
+
+  (def (secure-bytevector-ref sbv index)
+    (let* ((i (checked-index 'secure-bytevector-ref sbv index))
+           (tmp (secure-bytevector-load 'secure-bytevector-ref sbv)))
+      (dynamic-wind
+        (lambda () #f)
+        (lambda ()
+          (bytevector-u8-ref tmp i))
+        (lambda ()
+          (wipe-heap-bytevector! tmp)))))
+
+  (def (secure-bytevector-set! sbv index byte)
+    (let* ((i (checked-index 'secure-bytevector-set! sbv index))
+           (b (checked-byte 'secure-bytevector-set! byte))
+           (tmp (secure-bytevector-load 'secure-bytevector-set! sbv)))
+      (dynamic-wind
+        (lambda () #f)
+        (lambda ()
+          (bytevector-u8-set! tmp i b)
+          (secure-bytevector-store! 'secure-bytevector-set! sbv tmp))
+        (lambda ()
+          (wipe-heap-bytevector! tmp))))
+    (void))
+
+  (def (secure-bytevector->bytevector sbv)
+    (secure-bytevector-load 'secure-bytevector->bytevector sbv))
+
+  (def (secure-bytevector-wipe! sbv)
+    (secure-wipe (live-region 'secure-bytevector-wipe! sbv)))
+
+  (def (secure-bytevector-random-fill! sbv)
+    (secure-random-fill (live-region 'secure-bytevector-random-fill! sbv)))
+
+  (def (secure-bytevector-free sbv)
+    (secure-free (live-region 'secure-bytevector-free sbv)))
+
   (define-syntax with-secure-region
     (syntax-rules ()
       ((_ ((name size) ...) body ...)
@@ -74,4 +198,14 @@
            (lambda ()
              (secure-free name) ...))))))
 
+  (define-syntax with-secure-bytevector
+    (syntax-rules ()
+      ((_ ((name size) ...) body ...)
+       (let ((name (make-secure-bytevector size)) ...)
+         (dynamic-wind
+           (lambda () #f)
+           (lambda () body ...)
+           (lambda ()
+             (secure-bytevector-free name) ...))))))
+
   ) ; library (std crypto secure-mem)
diff --git a/scripts/security-check.sh b/scripts/security-check.sh
index 858d112..a75a9ee 100755
--- a/scripts/security-check.sh
+++ b/scripts/security-check.sh
@@ -138,10 +138,10 @@ if ! grep -Eq '^ARG BASE_IMAGE=.*@sha256:[0-9a-f]{64}$' Dockerfile.qt; then
 fi
 
 require_text supply-chain.lock 'jerboa_yubikey.commit=e448afca926ca9ad40fd98effa8d6264cb1019f5'
-require_text supply-chain.lock 'jerboa.version=v0.2.7'
-require_text supply-chain.lock 'jerboa.commit=73176e945f656334b8b1cf0d6b11c6c41f8ba427'
-require_text supply-chain.lock 'jerboa.tree=5e2ce0ac497d4b93d3a5859a13856fdbb0987ba3'
-require_text supply-chain.lock 'jerboa.bundle_sha256=ac8587f3b466f7d6d3047b4cafefd9985a08983503a5ad0764d4947cef25e828'
+require_text supply-chain.lock 'jerboa.version=v0.2.8'
+require_text supply-chain.lock 'jerboa.commit=ae716feb376ce9adefcad65e923ebfd866131947'
+require_text supply-chain.lock 'jerboa.tree=9fafa4b48f6a4d1c166dc3ae57be720237ee988b'
+require_text supply-chain.lock 'jerboa.bundle_sha256=6e9aae5475dff96e70f4d9710d47d37fa04eee8948775439f2d796ffb7e2ae8c'
 require_text supply-chain.lock 'jerboa.source_fallback=support/ensure-jerboa.sh'
 require_text supply-chain.lock 'container.ubuntu_24_04=sha256:'
 require_text supply-chain.lock 'container.ubuntu_apt_snapshot=20260701T000000Z'
diff --git a/supply-chain.lock b/supply-chain.lock
index 03e40b4..90f82bc 100644
--- a/supply-chain.lock
+++ b/supply-chain.lock
@@ -1,8 +1,8 @@
 # jerboa-browser effective release input lock (reviewed 2026-07-10)
-jerboa.version=v0.2.7
-jerboa.commit=73176e945f656334b8b1cf0d6b11c6c41f8ba427
-jerboa.tree=5e2ce0ac497d4b93d3a5859a13856fdbb0987ba3
-jerboa.bundle_sha256=ac8587f3b466f7d6d3047b4cafefd9985a08983503a5ad0764d4947cef25e828
+jerboa.version=v0.2.8
+jerboa.commit=ae716feb376ce9adefcad65e923ebfd866131947
+jerboa.tree=9fafa4b48f6a4d1c166dc3ae57be720237ee988b
+jerboa.bundle_sha256=6e9aae5475dff96e70f4d9710d47d37fa04eee8948775439f2d796ffb7e2ae8c
 jerboa.release_checksums=support/jerboa-release-sha256.txt
 jerboa.source_url=https://git.sr.ht/~lisp/jerboa
 jerboa.source_fallback=support/ensure-jerboa.sh
diff --git a/support/ensure-jerboa.sh b/support/ensure-jerboa.sh
index d06651d..720508e 100755
--- a/support/ensure-jerboa.sh
+++ b/support/ensure-jerboa.sh
@@ -173,7 +173,11 @@ ensure_source_checkout() {
     require_tool cargo
 
     cache_root=${JERBOA_SOURCE_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/jerboa-source}
-    src_dir="$cache_root/$version-$source_commit"
+    feature_suffix=
+    if [ "${JERBOA_NATIVE_FEATURES:-}" ]; then
+        feature_suffix="-features-$(printf '%s' "$JERBOA_NATIVE_FEATURES" | LC_ALL=C tr -c 'A-Za-z0-9_.=-' '_')"
+    fi
+    src_dir="$cache_root/$version-$source_commit$feature_suffix"
 
     if [ -d "$src_dir/.git" ]; then
         verify_source_checkout "$src_dir"
diff --git a/support/jerboa-release-sha256.txt b/support/jerboa-release-sha256.txt
index 33cfd7c..68d3374 100644
--- a/support/jerboa-release-sha256.txt
+++ b/support/jerboa-release-sha256.txt
@@ -8,3 +8,5 @@ v0.2.4 macos-arm64 2cef9bd20043165e87b318d971411c7c991a20dd61bda40918eeafaf6770e
 v0.2.4 linux-amd64 c43bc4d49a82baa868353ecadec87765213b1d2a048e24359afe14ca94799419
 v0.2.4 linux-arm64 94b8a813e0280b0e33d32088484c7a759e9c3018e384a6efb7c69a5a80d7566d
 v0.2.4 freebsd-amd64 ba6aff306af1fd2f479c86ce83becd5c3bb0a481a4dc1fe84fa520cc18da56b8
+v0.2.8 macos-arm64 8f1f13fd33c5260e04efba12dec9d4b7cfdfcf0765aa6b39d03eda66e2084daa
+v0.2.8 freebsd-amd64 a515152e55474b90e9053c53466d2870c3cbdc9d0f8d5a8b788f067c6ed8e073