Skip to content

chore(deps): bump sharp from 0.35.4 to 0.35.5 in /services/hosted in the npm_and_yarn group across 1 directory - #615

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/services/hosted/npm_and_yarn-2e99e09d87
Closed

dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/services/hosted/npm_and_yarn-2e99e09d87

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Bumps the npm_and_yarn group with 1 update in the /services/hosted directory: sharp.

Updates sharp from 0.35.4 to 0.35.5

Release notes

Sourced from sharp's releases.

v0.35.5

https://github.com/lovell/sharp-libvips/releases/tag/v1.3.4

  • Add upper bounds check on length of linear and GIF delay arrays.

  • Improve error handing when WebAssembly fallback also fails. #4593 @​lazerg

  • TypeScript: Allow multi-frame options for JXL output. #4602 @​ramin-010

  • TypeScript: Remove non-existent named export. #4604

  • Increase accepted dimensions when extending an image. #4605

  • Improve gain map support for extract and rotate operations. #4606

  • Tests: Ensure composite tests pass on big endian platforms. #4609

v0.35.5-rc.1

https://github.com/lovell/sharp-libvips/releases/tag/v1.3.4-rc.1

  • Add upper bounds check on length of linear and GIF delay arrays.

  • Improve error handing when WebAssembly fallback also fails. #4593 @​lazerg

  • TypeScript: Allow multi-frame options for JXL output. #4602 @​ramin-010

  • TypeScript: Remove non-existent named export. #4604

  • Increase accepted dimensions when extending an image. #4605

  • Improve gain map support for extract operation. #4606

... (truncated)

Commits
  • 51a990f Release v0.35.5
  • 96de105 Upgrade to sharp-libvips v1.3.4
  • 3a61390 CI: Configure Dependabot with all package.json locations
  • 4940c50 Improve gain map support for rotate/flip/flop ops
  • 20654aa Prerelease v0.35.5-rc.1
  • ef4f934 CI: Upgrade to Ubuntu 26.04
  • 358df95 Upgrade to libvips v8.18.7
  • 49f4903 Improve gain map support for rotate-then-extract #4606
  • 0e2e55e Silence a couple of compiler/static analysis warnings
  • cef3b8c Improve gain map support for extract operation #4606
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Summary by CodeRabbit

  • Chores
    • Updated the tooling used for hosted services.

Bumps the npm_and_yarn group with 1 update in the /services/hosted directory: [sharp](https://github.com/lovell/sharp).


Updates `sharp` from 0.35.4 to 0.35.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](lovell/sharp@v0.35.4...v0.35.5)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot requested a review from doublegate as a code owner October 18, 2026 21:37
@dependabot dependabot Bot added dependencies Dependency updates javascript Pull requests that update javascript code labels Oct 18, 2026
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​cloudflare/​vitest-plugin@​1.3.4 ⏵ 1.4.499 +110081 +199 +1100
Updatednpm/​wrangler@​4.145.0 ⏵ 4.151.198 +110092 +196100

View full report

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: doublegate/RustyNES/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4c3c6773-bc86-44bc-9708-62ea007110ba
📥 Commits

Reviewing files that changed from the base of the PR and between 1e68b51 and 187bd7a.

⛔ Files ignored due to path filters: 1
  • services/hosted/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing: 1
  • services/hosted/package.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The hosted package updates @cloudflare/vitest-plugin to 1.4.4 and wrangler to 4.151.1. Other listed tool versions remain unchanged.

Changes: Hosted dependency updates

Layer / File(s) Summary
Update hosted tooling
services/hosted/package.json
The manifest updates @cloudflare/vitest-plugin to 1.4.4 and wrangler to 4.151.1.

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~3 minutes

Change: Other

Merge Risk: ⚪ Minimal · up to 187bd

The hosted test tooling is updated to newer Cloudflare plugin and Wrangler releases. Their recorded compatibility requirements fit the unchanged Vitest version and hosted Node runtimes, leaving no concrete merge-blocking risk.

### Architecture Summary

Architecture risk: 🔵 Low · up to 187bd

The change affects 1 system.

Changed systems: services/hosted

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — services/hosted (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in services/hosted/package.json: Updated @cloudflare/vitest-plugin to 1.4.4 and wrangler to 4.151.1; the Workers types, TypeScript, and Vitest dependency versions remain unchanged.
Pre-merge checks: Passed 8 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Title check Warning The title states that the PR updates sharp from 0.35.4 to 0.35.5, but the reviewed changes update the Cloudflare Vitest plugin and Wrangler. The title does not describe the main changes shown in… Rename the PR to describe the Cloudflare Vitest plugin and Wrangler updates, or provide the sharp dependency change in the reviewed files.
Passed Passed checks: 8 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Docs-As-Spec Sync Passed The PR changes only services/hosted/package.json and services/hosted/package-lock.json. The manifest diff updates hosted development dependencies. No files under crates/rustynes-cpu, `crates/rus…
Changelog Entry For User-Visible Changes Passed PASS: The diff changes only services/hosted/package.json development-tool versions and their lockfile resolution. In the lockfile, sharp is pulled by miniflare, which is a dependency of `@cloudf…
No Unwrap/Expect/Panic On Untrusted Input Passed The pull request changes only services/hosted/package.json and services/hosted/package-lock.json. The diff contains dependency version and lockfile metadata updates only. It adds no .unwrap(), `…
Safety Comment On New Unsafe Blocks Passed The pull request changes only services/hosted/package.json and services/hosted/package-lock.json. The diff adds no Rust code, unsafe block, or unsafe fn, so the SAFETY-comment requirement is n…
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

Full details: Docs-As-Spec Sync

Explanation

The PR changes only services/hosted/package.json and services/hosted/package-lock.json. The manifest diff updates hosted development dependencies. No files under crates/rustynes-cpu, crates/rustynes-ppu, crates/rustynes-apu, or crates/rustynes-mappers changed, so the docs-as-spec requirement does not apply.

Full details: Changelog Entry For User-Visible Changes

Explanation

PASS: The diff changes only services/hosted/package.json development-tool versions and their lockfile resolution. In the lockfile, sharp is pulled by miniflare, which is a dependency of @cloudflare/vitest-plugin; hosted source has no sharp reference. CHANGELOG.md has an empty [Unreleased] section and is unchanged. This PR does not introduce a user-visible behavior change, feature, or user-facing bug fix.

Full details: No Unwrap/Expect/Panic On Untrusted Input

Explanation

The pull request changes only services/hosted/package.json and services/hosted/package-lock.json. The diff contains dependency version and lockfile metadata updates only. It adds no .unwrap(), .expect(), or panic!() calls, and it adds no code that processes untrusted input.

Full details: Safety Comment On New Unsafe Blocks

Explanation

The pull request changes only services/hosted/package.json and services/hosted/package-lock.json. The diff adds no Rust code, unsafe block, or unsafe fn, so the SAFETY-comment requirement is not triggered.

Full details: Title check

Explanation

The title states that the PR updates sharp from 0.35.4 to 0.35.5, but the reviewed changes update the Cloudflare Vitest plugin and Wrangler. The title does not describe the main changes shown in the changeset.

Resolution

Rename the PR to describe the Cloudflare Vitest plugin and Wrangler updates, or provide the sharp dependency change in the reviewed files.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

Bumps @cloudflare/vitest-plugin (1.3.4 to 1.4.4) and wrangler (4.145.0 to 4.151.1) in package.json to pull in sharp 0.35.5 along with updated transitive miniflare, workerd, esbuild, and zod packages in package-lock.json.

Blocking issues

None found.

Suggestions

  • Peer dependency mismatch on @cloudflare/workers-types: package.json and package-lock.json keep @cloudflare/workers-types pinned to 5.20260930.2, whereas wrangler@4.151.1 bumps its peer dependency requirement to "^5.20261006.1" (package-lock.json, post-diff line 2768) and workerd moves to 1.20261012.1. Because @cloudflare/workers-types is explicitly installed in devDependencies, npm 7+ will flag an ERESOLVE peer conflict on subsequent npm install runs unless @cloudflare/workers-types is also bumped to >=5.20261006.1 (ideally matching workerd at 5.20261012.1).

Nitpicks

  • Misleading PR title for scope of changes: The title claims only a patch bump to sharp (0.35.4 to 0.35.5), but the diff also upgrades direct devDependencies (wrangler 4.145.0 to 4.151.1, @cloudflare/vitest-plugin 1.3.4 to 1.4.4) and moves miniflare from 5.20260930.0-alpha to 5.20261012.1-beta (package-lock.json). Update the PR title/commit message before merging so the git history reflects the wrangler/workerd runtime update.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

doublegate added a commit that referenced this pull request Oct 19, 2026
Folds in the three open Dependabot PRs (#614 cosim sha2 0.11.1, #615
sharp 0.35.5, #616 the production-dependencies group) and takes the rest
of the tree to its newest compatible versions, checked rather than
assumed:

- Cargo: `cargo update` on the workspace (43 packages, among them sha2
  0.11.1, tokio 1.53.3, serde_json 1.0.154, proptest 1.12.0, cc 1.8.0,
  arboard 3.7.0, uuid 1.28, zip 9.0.5) and on rustynes-cosim; libc's
  requirement raised to 0.2.191 in rustynes-cheevos and
  rustynes-libretro, the only direct requirement `cargo upgrade
  --incompatible allow` found behind. `cargo update --verbose` holds
  four transitive packages, none ours to move: getrandom 0.2 and 0.3
  (the deliberate wasm32 shims in rustynes-frontend), generic-array
  0.14.9 and psm 0.1.32 (newer releases need a newer rustc than the
  MSRV-aware resolver allows). wasm-bindgen stays 0.2.129, equal to the
  Trunk.toml CLI pin.
- Rust 1.99.0 -> 1.99.1 (rust-toolchain.toml and .gitlab-ci.yml's
  RUSTUP_TOOLCHAIN together; every buildbot job runs `rustup toolchain
  install`, so a patch move needs nothing from the image). rust-version
  stays "1.99".
- dtolnay/rust-toolchain 7e38f4b4 -> 683b588c (refs/tags/v1). The four
  commits were read, as rust-setup's comment requires: `${{ }}`
  interpolations into run scripts moved into env variables (closing a
  script-injection path), the flag step folded into a bash array, 1.99.1
  in the patch table. taiki-e/install-action 2.87.28 -> 2.87.29; every
  other action's floating major already resolves to its newest release.
- Hosted Worker: TypeScript 5.9.3 -> 7.0.2, vitest 4.1.11 -> 5.0.5,
  @cloudflare/vitest-plugin 1.4.4 (peer: vitest ^4.1 || ^5),
  workers-types 5.20261019.1 (wrangler 4.151.1 wants >= 5.20261006.1),
  wrangler 4.151.1; package version 3.4.1. npm audit: 0
  vulnerabilities. `wrangler deploy --dry-run` bundles (37.45 KiB).
- ruff-pre-commit v0.16.10 -> v0.17.1 (passes on the repo).
- MkDocs on Python 3.15 (3.15.0 released 2026-10-09): verified first in
  a scratch venv, mkdocs-material[imaging] installs from wheels (Pillow
  13.0.0) and `mkdocs build --strict` completes with no warning.

Held, with reasons: Node 24 (26 becomes LTS on 2026-10-28), JDK 25 (the
current LTS; 27 is not), NDK r30 16248370 (already newest). Android
(Gradle 9.8.1, AGP 9.4.1, Kotlin 2.4.21, Compose BOM 2026.09.00, every
androidx and Play artifact) was already at its newest stable releases.

Verified on 1.99.1: fmt; clippy -D warnings for the workspace, the
scripting / hd-pack / retroachievements / full feature sets, both
wasm32 targets and rustynes-cosim; the thumbv7em no_std build; rustdoc
-D warnings; cargo test --workspace 3,183 passed / 0 failed / 9
ignored; the Worker's 114 tests and tsc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@doublegate

Copy link
Copy Markdown
Owner

Superseded by #618 (v3.4.1 "Beacon"), commit 005226c, which takes this update and every other dependency to its newest compatible version: both Cargo lockfiles updated (sha2 0.11.1 in the workspace and in rustynes-cosim, the production-dependencies group, and the rest), sharp 0.35.5 in services/hosted with TypeScript 7, vitest 5 and wrangler 4.151, Rust 1.99.1, and the newest action pins. Verified on 1.99.1: clippy -D warnings on every feature set and both wasm32 targets, the no_std build, rustdoc, and cargo test --workspace (3,183 passed, 0 failed).

@doublegate doublegate closed this Oct 19, 2026
@dependabot @github

dependabot Bot commented on behalf of github Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

This pull request was built based on a group rule. Closing it will not ignore any of these versions in future pull requests.

To ignore these dependencies, configure ignore rules in dependabot.yml

@dependabot
dependabot Bot deleted the dependabot/npm_and_yarn/services/hosted/npm_and_yarn-2e99e09d87 branch October 19, 2026 02:10
doublegate added a commit that referenced this pull request Oct 19, 2026
* docs(mister): release bitstreams are version-named on GitHub

On 2026-10-18 the maintainer chose the version-named style for every
release's bitstream assets, after first having the version-named
duplicates deleted the same day. All 49 datecoded .rbf assets on both
repositories were renamed in place (RustyNES_MiSTer-<tag>.rbf, and
RustyNES_MiSTer-v3.4.0-lite.rbf for v3.4.0's on-die build); the bytes and
the GitHub-recorded SHA-256 digests are unchanged, and 44 release bodies
were rewritten to name the attached file by its version. The sibling's
release-rbf.sh now uploads that form (sibling 4e71c79), while its
releases/ keeps the datecoded names Distribution_MiSTer parses.

Three docs here still described the brief datecoded-only state:
docs/mister.md, to-dos/mister/TASKS.md and the v3.x hardware-verification
plan. They now say: datecoded in the sibling's releases/, version-named
on the releases. .github/release-notes/v3.4.0.md is synced to the live
v3.4.0 body, which differs from the committed file in exactly the three
asset names (checked by diff, ignoring trailing blank lines GitHub
appended).

Verified: pre-commit markdownlint passes on all four files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(salvage): record the v340 scratch move

Records the post-release tmp-salvage pass over the v3.4.0 scratch tree
(moved into the gitignored salvaged/tmp-scratch/f63c2db0/ with relative
paths kept). The files themselves are untracked by design; this entry
is the only tracked record of what moved and from where.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(web): make the browser-cheevos build load rcheevos at all

Building the web frontend with `--features browser-cheevos`, as the
v3.4.1 hosted plan needs, found three packaging defects. Each one alone
stopped the browser RetroAchievements glue from ever reaching rcheevos;
none was visible to a gate, because no CI job builds that feature and
the Pages build uses the default set.

1. index.html carried `data-cargo-features=""`. Trunk lets that
   attribute override its command line, and an EMPTY value silently
   discarded `trunk build --features ...` and `--no-default-features`.
   Measured: the documented lightweight build (`--no-default-features
   --features wasm-canvas`) produced the default 12.6 MB winit build;
   with the attribute gone it produces 1,336,570 bytes, and the
   browser-cheevos build now emits the RA glue snippet. The Pages build
   (`trunk build --release`, default features) is unaffected, since an
   empty list on top of the defaults was the defaults. A comment at the
   site says why the attribute must stay absent.

2. ra_glue.js imported "./rcheevos.js". `#[wasm_bindgen(module = ...)]`
   makes trunk emit the glue as `snippets/<crate>-<hash>/web/cheevos/
   ra_glue.js`, and nothing copies the Emscripten module there, so the
   relative import resolved inside snippets/ and 404ed. The module is
   now resolved against the PAGE: `<meta name="rustynes-rcheevos-base">`
   when present, else `cheevos/` beside the page. `locateFile` points the
   loader at rcheevos.wasm under the same base, so both paths are stated
   in one place rather than relying on Emscripten resolving the .wasm
   against its own script.

3. build_rcheevos_wasm.sh emitted a MODULARIZE UMD module, which has no
   default export, so the glue's `const { default: ... } = await
   import(...)` got undefined. `-sEXPORT_ES6=1` makes it an ES module.

Verified in headless Chromium against a Pages-layout copy of dist/
(served on 127.0.0.1:18765): `ra_rcheevos_version()` returned 12.5 and
every request was 200. Control: the old glue against the same layout
404s on snippets/.../rcheevos.js. The README's wiring steps and the two
script-tag passages in docs/cheevos-browser.md are corrected to match;
markdownlint passes on both.

Not done, deliberately: browser-cheevos is NOT enabled on Pages and no
RA proxy meta tag is added. The maintainer is holding web RA until the
RetroAchievements integration request is sent; this change only makes
the feature work when it is turned on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* ci: bake the deployed hosted base into every shipped build

v3.4.0 shipped services/hosted/ as code with no workflow setting
RUSTYNES_HOSTED_BASE, because the maintainer was away and the deploy
moved to v3.4.1. The account is now provisioned (budget alerts, the TURN
key `rustynes-hosted`, its two secrets on the `rustynes-hosted` Worker),
so the shipped builds point at it:
https://rustynes-hosted.parobek.workers.dev (account subdomain
`parobek`).

Scope, as the maintainer chose it: the Pages build (web.yml), the
desktop release binaries (release.yml), the Android Play and sideload
bridge builds (android.yml, cargo-ndk) and the iOS TestFlight
xcframework (ios.yml). F-Droid is excluded; its recipe builds from source
under its own rules. Each workflow gets the variable in its top-level
env block, so every job in it compiles the same base and no step can
silently miss it.

Why env and not a flag: rustynes-netplay reads it with option_env!, so
it is a compile-time constant (BUILD_HOSTED_BASE). A build without it
keeps v3.4.0's behaviour, and a user's `hosted_base_url` config still
wins over the baked value. ci.yml is deliberately left alone:
hosted.rs's a_stock_build_has_no_hosted_base asserts the constant is
None, guarded on the variable being absent at test time, so setting it
there would turn that test into a no-op rather than fail it.

The Worker's ALLOWED_ORIGINS already lists https://doublegate.github.io
and index.html carries no CSP, so the Pages origin needs nothing more.
The live smoke gates (healthz, the lobby test, POST /turn-credentials,
the rate limit, the kill switch) run against the deploy, not here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* feat(hosted): deploy configuration for v3.4.1, RA proxy held off

The v3.4.1 deploy puts the lobby and TURN credentials live and leaves
the RetroAchievements proxy switched off. The maintainer is holding web
RA until RetroAchievements answers the integration request (D31), and
the Pages build ships without browser-cheevos, so no client would use
the proxy; leaving it enabled would only let anyone forward RA calls
under the RustyNES/ User-Agent before RA has agreed to that. RA_ENABLED
is therefore "false" in wrangler.jsonc, with the reason at the site.
ra.ts refuses every /ra/ request unless the value is exactly "true", so
the off state is the kill switch path, already tested. Turning it on is
a dashboard variable change, no code.

RA_USER_AGENT moves to RustyNES/3.4.1 (Web wasm32) rcheevos/12.5.0, the
release this config ships in. bump_release.py does not touch it, so it
is bumped by hand.

The Worker tests run against wrangler.jsonc's own vars, so turning the
proxy off there would have stopped the proxy being tested at all.
vitest.config.ts now binds RA_ENABLED "true" for the tests (the
kill-switch test still sets "false" itself). Mutation: with that
binding flipped to "false", 26 of 111 tests fail; with it "true", 111
pass. tsc --noEmit is clean.

Also: docs/agents/tooling-traps.md records the trunk trap behind the
earlier web fix (an empty data-cargo-features attribute silently
overrides --features and --no-default-features).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(plan): the v3.4.1 plan and its CHANGELOG entries

to-dos/plans/v3.4.1-plan.md records the patch release v3.4.0 left: the
hosted deploy (done 2026-10-18, live gates passed: healthz, the live
lobby test, credentials with ttl 10800 that relay in a browser, the
429 on the 7th request, the 405, the kill switch), the client wiring,
the browser RA packaging fix, the two save-engine defects and the two
refusal findings (sibling 419ee1b, 9e7ef9c), the version-named release
assets, and what stays open: the credential-expiry check after
2026-10-19T02:44Z, the two-browser and two-NAT sessions (maintainer),
the RA unlock (held for D31), the v3.4.1 bitstreams and the
release-rbf.sh run before the release PR merges.

It also records the agent's call that follows from holding web RA: the
Worker is deployed with RA_ENABLED false.

CHANGELOG [Unreleased] gains Added / Changed / Fixed entries for the
same. release_anchor_audit (14), release_state_prose_audit (16),
contribution_checklist_audit (4) and release_notes_render_audit (9)
pass; markdownlint passes on both files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* chore(release): bump to v3.4.1 "Beacon"

The CHANGELOG [Unreleased] entries become [3.4.1] - 2026-10-19 -
"Beacon" (the codename the maintainer chose), and
scripts/release-automation/bump_release.py moved every anchor from it:
the workspace and cosim manifests and both lockfiles, the libretro
.info display_version, Android versionName 3.4.1 / versionCode 30401,
iOS MARKETING_VERSION, and the current-release prose in AGENTS.md,
README, ROADMAP, SECURITY, SUPPORT, OVERVIEW, ARCHITECTURE, STATUS and
VERSION-PLAN, each demoting "Waypoint" rather than overwriting it.

The script first refused: to-dos/ROADMAP.md's release-line chain still
ended "v3.4.0, the current release". That chain now has a v3.4.1 clause
saying what the release did, and v3.4.0's clause lost the tail.
VERSION-PLAN's v3.4.1 row is marked shipped with what it shipped, and a
v3.4.1 history row sits above v3.4.0's.

The date is 2026-10-19 because the MiSTer bitstreams are compiled that
day: BUILD_DATE is the wall-clock date, and a compile on a different
day is a different design and a different file name.

Verified: every harness audit target (12) passes, among them
release_anchor_audit 14, release_state_prose_audit 16; markdownlint
passes. .github/release-notes/v3.4.1.md is written after the compiles,
when its figures exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* build(deps): every dependency at its newest compatible version

Folds in the three open Dependabot PRs (#614 cosim sha2 0.11.1, #615
sharp 0.35.5, #616 the production-dependencies group) and takes the rest
of the tree to its newest compatible versions, checked rather than
assumed:

- Cargo: `cargo update` on the workspace (43 packages, among them sha2
  0.11.1, tokio 1.53.3, serde_json 1.0.154, proptest 1.12.0, cc 1.8.0,
  arboard 3.7.0, uuid 1.28, zip 9.0.5) and on rustynes-cosim; libc's
  requirement raised to 0.2.191 in rustynes-cheevos and
  rustynes-libretro, the only direct requirement `cargo upgrade
  --incompatible allow` found behind. `cargo update --verbose` holds
  four transitive packages, none ours to move: getrandom 0.2 and 0.3
  (the deliberate wasm32 shims in rustynes-frontend), generic-array
  0.14.9 and psm 0.1.32 (newer releases need a newer rustc than the
  MSRV-aware resolver allows). wasm-bindgen stays 0.2.129, equal to the
  Trunk.toml CLI pin.
- Rust 1.99.0 -> 1.99.1 (rust-toolchain.toml and .gitlab-ci.yml's
  RUSTUP_TOOLCHAIN together; every buildbot job runs `rustup toolchain
  install`, so a patch move needs nothing from the image). rust-version
  stays "1.99".
- dtolnay/rust-toolchain 7e38f4b4 -> 683b588c (refs/tags/v1). The four
  commits were read, as rust-setup's comment requires: `${{ }}`
  interpolations into run scripts moved into env variables (closing a
  script-injection path), the flag step folded into a bash array, 1.99.1
  in the patch table. taiki-e/install-action 2.87.28 -> 2.87.29; every
  other action's floating major already resolves to its newest release.
- Hosted Worker: TypeScript 5.9.3 -> 7.0.2, vitest 4.1.11 -> 5.0.5,
  @cloudflare/vitest-plugin 1.4.4 (peer: vitest ^4.1 || ^5),
  workers-types 5.20261019.1 (wrangler 4.151.1 wants >= 5.20261006.1),
  wrangler 4.151.1; package version 3.4.1. npm audit: 0
  vulnerabilities. `wrangler deploy --dry-run` bundles (37.45 KiB).
- ruff-pre-commit v0.16.10 -> v0.17.1 (passes on the repo).
- MkDocs on Python 3.15 (3.15.0 released 2026-10-09): verified first in
  a scratch venv, mkdocs-material[imaging] installs from wheels (Pillow
  13.0.0) and `mkdocs build --strict` completes with no warning.

Held, with reasons: Node 24 (26 becomes LTS on 2026-10-28), JDK 25 (the
current LTS; 27 is not), NDK r30 16248370 (already newest). Android
(Gradle 9.8.1, AGP 9.4.1, Kotlin 2.4.21, Compose BOM 2026.09.00, every
androidx and Play artifact) was already at its newest stable releases.

Verified on 1.99.1: fmt; clippy -D warnings for the workspace, the
scripting / hd-pack / retroachievements / full feature sets, both
wasm32 targets and rustynes-cosim; the thumbv7em no_std build; rustdoc
-D warnings; cargo test --workspace 3,183 passed / 0 failed / 9
ignored; the Worker's 114 tests and tsc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(netplay): let the mobile apps use the compiled-in hosted lobby

Codex and Copilot on #618: baking RUSTYNES_HOSTED_BASE into the Android
and iOS CI builds made no difference to a fresh install, because both
apps stopped short of the bridge. NpNetConfig::to_nat_config substitutes
the hosted lobby only for an EMPTY signaling URL, and:

- Android's Settings default is the placeholder
  `wss://relay.rustynes.example`. netplayConfig() forwarded it
  (non-empty, so never substituted) and MainActivity disabled room-code
  play on it.
- iOS's `signalingConfigured` accepted only an explicit ws:// or wss://
  URL, so NetplayView disabled both room buttons and hostRoom/joinRoom
  returned before reaching the bridge.

The fix gives the shells the fact they lacked:

- rustynes-mobile exports `np_hosted_available()` (true exactly when
  `hosted::resolve_base("")` finds a compiled-in base); UniFFI makes it
  `npHostedAvailable()` in Kotlin and Swift. Test:
  hosted_availability_follows_the_compiled_base pins it to
  BUILD_HOSTED_BASE.
- Android: `userSignalingUrl()` maps the placeholder (and blank) to "no
  override", so the bridge fills in the hosted lobby; any other value is
  the user's relay and wins. `onlineNetplayConfigured()` = a user relay
  OR a compiled-in base, replacing MainActivity's inline check.
- iOS: an empty URL counts as configured when npHostedAvailable(); an
  explicit URL is still checked for a WebSocket scheme. The Settings
  header reads "(optional)" in such a build.

A build without a base (F-Droid, plain source) behaves exactly as
before: the placeholder still means unconfigured and the caveat shows.

Verified: clippy and rustdoc clean for rustynes-mobile; its five hosted
tests pass. NOT compiled here: the Kotlin and Swift halves (no Xcode on
Linux). android.yml builds the Kotlin on the PR; ios.yml is dispatched
once on this branch before merge to compile the Swift (a one-time check
at the maintainer's direction; Apple runs are not a default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(web): trim the rcheevos base and share an in-flight module load

agy on #618, on the v3.4.1 glue change:

- rcheevosBase() did not trim the `rustynes-rcheevos-base` meta value
  (resolveProxyBase() trims its own). A value with surrounding spaces
  produced a URL with the spaces in it, and a whitespace-only value was
  taken as a base. It is trimmed now, and blank falls back to `cheevos/`.
- loadModule() cached only the finished module, so ra_init() and
  ra_rcheevos_version() called before the first load finished each
  imported and instantiated their own rcheevos.wasm. The in-flight
  promise is cached; a failed load clears it so a later call can retry.
- rcheevosBase is exported, and services/hosted/test/ra-glue.test.ts
  covers it with a stubbed `document` (the Workers runtime has no DOM):
  the default resolves against the page, not the snippet; the meta value
  is trimmed and given a trailing slash; a blank value counts as absent.
  Mutation: `.trim()` removed -> 2 of the new tests fail.

Verified: the Worker suite 114 passed, tsc (TypeScript 7.0.2) clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs: answer #618's documentation findings and record the review work

- .github/release-notes/v3.4.0.md (CodeRabbit): the "Two builds" line
  named the date-coded files without saying where they live; it now says
  MiSTer's updater installs them from the RustyNES_MiSTer repository's
  `releases/`, and that the attached files carry the same bytes under
  version names. The live v3.4.0 release body was updated from the file.
- docs/cheevos-browser.md:200 (CodeRabbit, outside the diff): the step
  told maintainers to set `RA_PROXY_BASE` in ra_glue.js, which no longer
  exists as a setting; it now names the `rustynes-ra-proxy-base` meta tag
  that resolveProxyBase() reads.
- to-dos/plans/v3.4.1-plan.md (CodeRabbit): row 10 split into 10a-10e so
  the waived checks read as waived (the seed sweep skipped and the
  ladders not waited for, both by the maintainer on 2026-10-18) and the
  remaining ones (one compile per build, VERIFY=1 at the new pin) carry
  their own status; rows 12-14 record the mobile, PGO and dependency
  work.
- CHANGELOG: the Added entry now says the profile-guided Linux binary
  and every CI-built Android artifact (the FOSS flavour too, agy's
  nitpick) carry the base, and that a fresh mobile install can host and
  join; a Changed entry records the dependency sweep.

markdownlint passes on every file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(release): keep the hosted base in the promoted PGO binary

Copilot and Antigravity on #618: release.yml set RUSTYNES_HOSTED_BASE in
its workflow-level env, but its `pgo` job calls the reusable pgo.yml,
and a called workflow inherits no `env:`. When the PGO gate promoted,
pgo-linux-asset replaced the Linux release archive (`--clobber`) with a
binary compiled without the base, silently dropping the hosted lobby
from the one platform whose binary is rebuilt.

- pgo.yml's workflow_call gains a `hosted_base` input (default empty),
  exported as RUSTYNES_HOSTED_BASE for the instrument/train/rebuild step
  only, and only when non-empty (an empty string would reach option_env!
  as a value). The dispatch-only BOLT job is unchanged.
- release.yml passes the URL as a literal: `env` is not an allowed
  context in a reusable workflow's `with:`.

Two literals that must agree are a drift waiting to happen, so
release_anchor_audit gains:

- every_shipped_build_compiles_the_same_hosted_base: web, release,
  android and ios set RUSTYNES_HOSTED_BASE exactly once, release.yml
  passes hosted_base exactly once, all name the same https URL, pgo.yml
  exports the input, and ci.yml sets none (a_stock_build_has_no_hosted_base
  asserts the default only when the variable is absent). Mutation: the
  PGO literal changed to another host -> FAILED; restored -> 18 passed.
- the_hosted_worker_names_the_workspace_version (agy's suggestion):
  wrangler.jsonc's RA_USER_AGENT, package.json's root and both
  package-lock root entries name the workspace version. bump_release.py's
  MANIFESTS now moves all four (each pattern matches exactly once; the
  lockfile entries carry the name line, since a bare "version" matches
  dependencies too). --selftest passes.

actionlint passes on both workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(salvage): record the pre-reboot scratch salvage

29 files moved into the gitignored salvaged/tmp-scratch/f63c2db0/ under the
maintainer's scope (this session, strong, only what cannot be rebuilt):
the pre-rewrite snapshots of every release body in both repositories (the
only copy, as GitHub keeps no body history), the v3.4.0 session notes and
the v4 research write-up. The manifest entry records how 4,629 candidates
were cut to 29 by measurement (505 byte-identical to git blobs, the rest
run output, generated code, downloads or already-published text) and what
was left, including a live TURN credential that must not be kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mobile): describe the hosted defaults in both settings screens

CodeRabbit on #618, round 2: with 0a4c6e9 a build that carries a hosted
base uses the hosted lobby for an empty signaling URL and fetches TURN
credentials when no TURN trio is set, but both apps' settings still said
otherwise.

- iOS SettingsView: the signaling footer said "the maintainer must deploy
  and host it", and the TURN footer said that empty fields mean
  punch-or-fail. In a build where npHostedAvailable() is true they now
  say to leave the fields empty for the hosted lobby and short-lived
  credentials, or to fill them in to use another server. A build without
  a base keeps the old wording, which is still accurate there.
- Android Settings: the online-netplay hint ("the default URL is a
  placeholder") gains a hosted variant, settings_netplay_online_hint_hosted
  in values/ and values-es/, chosen the same way.

Also, CodeRabbit's outside-diff note on SECURITY.md: the matrix still
named 3.3.x "the current line" (v3.4.0's cut missed it too). 3.4.x is now
current and 3.3.x moves to "Partial" with every older line.

Not compiled here: the Swift and Kotlin (ios.yml's one-time dispatch run
39188696837 predates this change; android.yml builds the Kotlin on the
PR). markdownlint passes on SECURITY.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(release): ship the PGO binary Stage 2 built, not Stage 4's test build

agy on #618 (round of 02:28): pgo.yml's Stage 2 builds
target/<triple>/release/rustynes with `--features full` (and, since
fecc1e5, the hosted base). Stage 4 then runs `cargo pgo optimize test
-- --workspace --release --features test-roms`, and cargo builds a
package's binaries for its integration tests, so it rebuilt the same
path with test-roms only and no base. Stage 6 uploaded that path. Every
promoted PGO Linux archive therefore shipped the test build: no
RetroAchievements, scripting, script-ipc, HD packs, debug hooks or AV
recording, and from v3.4.1 no hosted lobby. That half of the defect
predates this release; the promotion threshold seldom passed, which is
why no user report traced it.

- Stage 2 copies the binary it built to pgo-out/rustynes before
  anything else runs, and the upload reads that copy.
- The promotion gate refuses (never fails the job, which would redden
  a release whose plain assets are fine) when a hosted base was asked
  for and the kept binary does not contain it: option_env! compiles the
  URL in as a literal, so `grep -aF` finds it.
- release_anchor_audit now pins the copy, the upload path, the gate's
  check, and the absence of the old target/ upload path. Mutation: the
  old path put back -> FAILED; restored -> 18 passed. Its workflow
  value parser now drops a trailing YAML comment and either quote style
  (agy's nitpick).

agy also suggested hoisting the base into job-level env for Stage 4 and
the BOLT job. Not done: Stage 4 must keep running
a_stock_build_has_no_hosted_base with the variable absent, and the BOLT
job is workflow_dispatch-only (a dispatch run passes no base).

Also from the same review, the mobile follow-ups:
- Android: a custom relay must be a ws:// or wss:// URL to enable room
  codes, matching iOS; the Settings field no longer shows the dead
  placeholder URL (a stored placeholder reads as empty, "no override").
- iOS: the room-code hint and the Settings warning tell a hosted build's
  user to clear an invalid URL to get the hosted lobby back.

actionlint passes. Kotlin and Swift are not compiled here: android.yml
runs on this PR; the Swift change is two text expressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(ppu): make the emphasis table a module so CodeQL can parse it

The code-scanning banner ("CodeQL is reporting errors") traced to the
Rust extractor, which reported 25 of 544 files "extracted with errors"
on main and on #618 alike. Reproduced locally with the CodeQL 2.27.2
bundle (checksum-verified) against the same tree: 0 extraction errors
and 138 extraction warnings, in exactly 25 distinct files, so the
status page's count is the warning count.

101 of the 138 came from crates/rustynes-ppu/src/emphasis_delta_table.rs:
100 "expected an item" plus "Too many diagnostic messages". The file was
a bare array expression, valid only through `include!` in emphasis.rs,
and the extractor parses every `.rs` file as a module. Renaming it to
`.in` was tried and does not help: that extractor loads only `.rs`
files, so `include!` then failed inside emphasis.rs instead ("failed to
load file", "the macro 'include' expands to ERROR").

The fix makes it an ordinary module: emphasis_delta_table.rs now holds
`pub const EMPHASIS_DELTA_TABLE: [[i16; 3]; 512] = [...]` (rows byte
for byte as before), lib.rs declares `mod emphasis_delta_table;`, and
`EMPHASIS_DELTA` is that constant. The generator,
print_emphasis_delta_table, emits the module form, and its output is
byte-identical to the committed file (diffed).

Verified: the 118 rustynes-ppu tests pass, including
the_committed_table_is_the_documented_model; clippy -D warnings and
rustdoc -D warnings clean; the thumbv7em no_std build. CodeQL rerun on
the changed tree: 0 errors, 36 warnings, 24 files. The 24 left are 18
third-party macros the build-less extractor cannot expand (criterion,
bitflags, proptest, uniffi, retro_core) and 6 stray .rs files outside
any Cargo target (three under scripts/, three under tests/framework/),
a cleanup that is the maintainer's to decide. Whether the banner itself
clears can only be seen after main's next analysis; the status page is
not readable through the API.

Also: the v3.4.1 CHANGELOG entries for the PGO fix (with the v2.9.6
promotion found in the release history), the Android and iOS settings
follow-ups, the real 10027 fix in the MiSTer core, and this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted): revoke each TURN credential when its TTL ends

The v3.4.1 live expiry gate failed (T-HOSTED-TURN-EXPIRY). The Worker
asks Cloudflare for credentials with {"ttl": 10800}, and Cloudflare's
generate-credentials page says the ttl expires the credential and its
FAQ that an expired allocation is disconnected "after a short delay".
Measured on 2026-10-19 with relay-only ICE gathers in fresh Chromium
profiles, it does not refuse them: a 3-hour credential 2 h 38 min past
its expiresAt, and a 600 s credential (minted on a briefly redeployed
Worker, production restored within the minute) 6.6 minutes past its
own, both still obtained `relay udp` candidates. The spend guards bound
how many credentials exist; nothing bounded how long a leaked one
worked. The maintainer chose to fix it in v3.4.1 rather than ship it as
a known issue.

The Worker now enforces the TTL itself:

- handleTurn hands every TURN username in the minted ICE list to the
  global Budget object with its expiry (expiresAt * 1000).
- Budget.scheduleRevoke stores `revoke:<due ms, 15 digits>:<username>`
  (keys sort by due time, and survive eviction) and pulls the object's
  single alarm in to the due time if it was later.
- The alarm now revokes due credentials before its hourly sweep:
  POST rtc.live.cloudflare.com/v1/turn/keys/<id>/credentials/<user>/revoke
  with the key's token. 204 (and 404, which means Cloudflare no longer
  knows it) deletes the entry; anything else is retried every 5 minutes,
  REVOKE_MAX_ATTEMPTS (6) attempts in all, then dropped rather than
  retried forever. At most 50 per alarm, so one alarm stays short; the
  rest follow a second later. The alarm re-arms for the earlier of the
  next sweep and the next revocation.
- A failed mint schedules nothing. Revocation is never earlier than the
  TTL, so a session within its three hours is untouched.

Tests (TDD: the four new behaviours failed first): scheduling at
expiry, nothing on a failed mint, revoking only due credentials with
the bearer token and re-arming for the next one, a 404 as done, retry
then give-up. Mutations: the scheduling call removed -> 1 fails; the
re-arm ignoring the next revocation -> 2 fail. 119 passed; tsc
(TypeScript 7.0.2) clean.

PRIVACY.md now says the service keeps each credential's username (a
random identifier Cloudflare issues, not linked to the user or their
address) until the TTL ends, then revokes it and deletes the username;
the secret half is never stored. The README's guard table, its "Why 3
hours", backstop and live-gate steps, and turn.ts's TTL comment say the
TTL is enforced by revocation and why.

Not covered: credentials minted before this deploy (yesterday's gates
and today's two tests) are not in the schedule and stay usable until
whatever limit Cloudflare applies; none was shared beyond this machine.
The live re-run of the expiry gate follows the deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted): never drop a TURN revocation silently or delay it

The automated security review of 960ff9f raised two findings; both
hold.

1. Fail-open retries. A missing key, a 401 or a 403 used up the six
   attempts in 30 minutes and then deleted the revocation, and a 404
   was accepted as done with no trace, so a token rotated or revoked
   by mistake would have let every credential minted meanwhile live
   on. Now:
   - 2xx: done. 404: done (Cloudflare does not know the credential),
     logged as a warning, because a 404 from a wrong key id would look
     the same.
   - Missing key / 401 / 403: a configuration fault, logged as an
     error, retried hourly WITHOUT counting as an attempt.
   - Anything else: backoff from 5 minutes, doubling to an hour.
   - An entry is dropped only REVOKE_GIVE_UP_MS (48 h, Cloudflare's
     maximum TTL) past the credential's expiry, with an error log.
   A retry is re-keyed under its next attempt time, so the key order
   stays the order of work; the value keeps `dueMs` (the original
   expiry) and the attempt count. Logs carry the status and attempt
   count only, never the username, and Workers Logs are off in
   production.

2. A delayed revocation. alarm() computed the next revocation before
   awaiting Cloudflare; a credential minted during those awaits set its
   own earlier alarm, which the final setAlarm then pushed later (up to
   the next hourly sweep, past a 600 s TTL). The re-arm now reads the
   first pending key fresh after the sweep, and keeps any alarm already
   set earlier.

Tests: backoff and re-keying, kept until 48 h past expiry and dropped
after, 401/403 retried hourly with attempts unchanged, and a credential
scheduled from inside the revoke call (a mint landing mid-alarm) keeping
its alarm. Mutations: the fresh re-arm read removed -> 1 fails;
configuration failures counted as attempts -> 1 fails. 121 passed; tsc
clean. Deployed (version 6496919c) ahead of the live expiry re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mobile,web): an unusable relay URL disables room codes on Android

Review of 148ee28 on #618:

- Cursor Bugbot (medium): f6cb343 made Android's
  onlineNetplayConfigured require a ws:// or wss:// relay, "matching
  iOS", but on Android it only chose the caveat text; the Host online
  and Join buttons still called the bridge with the malformed URL, and
  the caveat said "no relay is configured". Both buttons are now
  disabled while it is false, as iOS disables its room buttons, and in
  a build that carries the hosted lobby the caveat says the Settings URL
  is not a WebSocket address and that clearing it restores the hosted
  lobby. A build without a base keeps the old caveat.
- Antigravity (suggestion): rcheevosBase() passed the meta value
  straight to `new URL(...)`, so a malformed value threw out of
  ra_init. It now falls back to `cheevos/` beside the page, as
  resolveProxyBase() refuses a bad proxy base. New test: a value of
  "http://[bad" resolves to the default (122 passed).

Not changed: agy's nitpick that the package-version anchors in
release_anchor_audit and bump_release.py match "\n" and would miss on a
CRLF checkout. Both read files this repository stores with LF
(.editorconfig mandates LF), and a CRLF checkout makes the audit FAIL,
loudly, rather than pass wrongly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted): ship TURN switched off; Cloudflare honours revoked credentials

Revocation (960ff9f, 2f78987) was deployed and tested live on
2026-10-19 with a 600 s credential and `wrangler tail` attached: the
Budget alarm fired at expiry and logged "turn revoke: 1 revoked" (a 2xx
from Cloudflare's revoke endpoint; a 404 would have logged its own
warning). Two minutes later the credential still obtained 8 `relay udp`
candidates in a fresh Chromium profile. So did the earlier 600 s
credential, revoked about 15 minutes before, and the 3-hour credential
from 14:35, expired since 16:35. The control: a credential with a
random username and password got NO relay candidate, so Cloudflare does
authenticate and the gather does distinguish. Cloudflare creates
allocations for credentials that are expired AND revoked, at least for
these intervals, contrary to its generate-credentials page and FAQ.
Its FAQ also says an expired credential stops being billed at once and
is disconnected shortly after; neither was measured.

The maintainer chose to ship with the relay off:

- wrangler.jsonc: TURN_ENABLED "false", with the reason at the site.
  Deployed (version 764bc2e4); POST /turn-credentials answers 503
  turn-disabled (after about 15 s of propagation, during which one
  credential was issued; it is scheduled for revocation and its
  response was not kept). Clients already treat turn-disabled as
  "connect directly (STUN only)".
- vitest.config.ts binds TURN_ENABLED "true", as for RA_ENABLED, so the
  TURN and revocation code stays under test (122 passed).
- budget.ts logs a count after a revocation pass ("turn revoke: N
  revoked"), no usernames, so an operator tailing the Worker can see it
  work; that line is how the 2xx above was established.
- Docs: the release headline in every anchor (AGENTS, README, ROADMAP,
  SUPPORT, OVERVIEW, VERSION-PLAN, STATUS, to-dos/ROADMAP, CHANGELOG),
  the CHANGELOG entry and the release notes say the lobby is live and
  the relay is deployed but off, and why; the Worker README gains "TURN
  is off"; plan row 3 records both failed gates; T-HOSTED-TURN-EXPIRY
  now lists what remains (measure billing and allocation lifetime,
  report to Cloudflare, switch back on).

Verified: release_anchor_audit 18, release_notes_render_audit 9 and
release_state_prose_audit pass; markdownlint passes on every changed
file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs: v3.4.1 verification figures, and the TURN backstop and research

- .github/release-notes/v3.4.1.md: Verification filled from the full
  local suites at 148ee28 on Rust 1.99.1 (no Rust file has changed
  since): test-roms 3,550 passed / 0 failed / 13 ignored over 172
  binaries, with commercial-roms 3,801 / 0 / 18 over 182. The iOS line
  says what was compiled: the one CI run built 3a6f97e; f6cb343's
  later Swift edits (two hint strings, a label) were not rebuilt.
- services/hosted/README.md: the backstop step now says expiry and
  revocation were measured not to stop new allocations, that deleting
  the TURN key is the likely stop (untested), and that the budget alert
  is the backstop known to work.
- to-dos/ROADMAP.md: T-HOSTED-TURN-EXPIRY gains the key-deletion test,
  the stricter two-peer billing test, and the alternatives from the
  2026-10-19 research (self-hosted eturnal with strict_expiry on a
  throttling VPS; STUN plus user-supplied TURN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted,mobile): answer #618's latest review round

CodeRabbit on d215275 (six inline, one outside the diff), all valid:

- budget.ts (outside the diff, CWE-359): admit() armed the hourly sweep
  only when no alarm was set. Since revocations share the object's one
  alarm, a pending revocation hours out could keep a salted IP hash past
  PRIVACY.md's "at most two hours". It now re-arms for the sweep when
  the set alarm is more than an hour away. Test (failed first): a
  revocation 3 h out, one admission, the alarm within the hour.
  123 passed.
- Netplay.kt / NetplayModel.swift: a bare "ws://" passed the prefix
  check and enabled room codes with no host. Android now requires
  `^wss?://` plus a host before any path; iOS parses with URL and
  requires a ws/wss scheme and a non-empty host. The Swift is not
  compiled here.
- services/hosted/README.md: every passage that said revocation bounds
  a credential now says it is attempted and that its effect is
  unverified (the guard table row, the revocation paragraph, "What the
  guards do not bound", the backstop step), the live-gate step says it
  still fails as of v3.4.1, the guard table shows the deployed
  TURN_ENABLED and RA_ENABLED values ("false"), and the retry policy
  is described as budget.ts implements it (2xx / 404 done, config
  errors hourly without counting, backoff 5 min doubling to 1 h, drop
  48 h past expiry).
- CHANGELOG: revocation described as requested, not effective.
- PRIVACY.md: the username retention states the 48 h worst case and the
  configurable lifetime (3 h deployed, up to 24 h), and that with the
  relay off nothing is kept; effective date 2026-10-19 (v3.4.1). The
  release notes state the change, as the policy itself requires.
- The release notes' iOS line also names the relay-URL check among the
  uncompiled Swift edits. The [GATES] finding was already fixed in
  d215275.

Also to-dos/mister/TASKS.md: T-MISTER-SS-ABANDON-STATES for v3.4.2,
Antigravity's blocking claim on sibling #67 read against the RTL (the
four unlisted states are safe as shipped; the comment is wrong and an
earlier abandon in E_PUT would keep the previous slot), deferred
because any rtl/ edit makes release-rbf.sh refuse the compiled
bitstreams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted,mobile): take a fresh clock after revocations; tighten relay URLs

Answers Antigravity's latest #618 round (no blocking findings) and the
older round's copy note.

Worker (services/hosted/src/budget.ts):

- alarm() read `now` once and reused it after up to REVOKE_BATCH (50)
  revocation fetches, each bounded by REVOKE_TIMEOUT_MS. A slow batch
  could therefore schedule the next alarm, and judge the 48 h give-up,
  against a clock minutes stale. revokeDue now takes Date.now() after each
  revokeOne for the give-up and the retry time, and the alarm's final
  scheduling reads the clock again before nextRevocation().
- revokeDue listed every pending revocation key and stopped after a
  batch. The list is now bounded ({ prefix, limit: REVOKE_BATCH + 1 });
  keys sort by attempt time, so the first batch is the right one.
- A retry wrote the new key after deleting the old one. An eviction
  between the two lost the revocation; the order is now put then delete,
  whose worst case is one harmless duplicate revoke.
- revokeKey truncates the time before zero-padding, so a fractional
  millisecond can never widen the fixed 15-digit field and break the
  lexical order the list relies on.

TURN stays switched off in production (TURN_ENABLED "false"); this code
runs only when it is re-enabled, and vitest binds it on.

Mobile:

- Android RELAY_URL accepted "wss://:8080" (no host) and whitespace after
  the host. The host must now start with a non-colon character and no
  whitespace is allowed anywhere; case-insensitive. Checked: ws://,
  wss://:8080 and "wss://h/p q" refused; wss://relay.example,
  WSS://Relay.example/x and ws://10.0.0.2:9000 accepted.
- iOS compares the URL scheme case-insensitively.
- Copy no longer promises a hosted relay or short-lived TURN credentials
  while the relay is off (Android EN/ES hint, iOS TURN footer), and the
  non-hosted Android hint no longer calls the empty default a placeholder.

Verified: vitest 123 passed; tsc --noEmit clean. No Rust changed. The
Kotlin and Swift edits are uncompiled locally (no Android/iOS run for
this commit; the release runs ios.yml).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* test(hosted): never arm a past alarm in the revocation tests

CI on ce80d0f failed "the alarm revokes every due credential ... and
only due ones": runDurableObjectAlarm returned false. The test's seed()
armed the Durable Object alarm at the credential's due time, which the
tests set in the past. workerd fires a past-due alarm by itself, so it
could run (and clear) between seed() and the test's explicit
runDurableObjectAlarm, which then found no alarm. Locally the race was
lost every time (123 passed); on the CI runner it was won once.

seed() now arms at max(due, now + 60 s). runDurableObjectAlarm runs an
armed alarm immediately regardless of its time, so every test still
drives the alarm explicitly, and the automatic one can no longer fire
first. The first test's final assertion (next alarm = the later
credential's due time, an hour out) is unaffected: that key's time is
later than the 60 s floor.

No production code changed. Verified: vitest 123 passed, three runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(hosted): withhold a credential whose revocation was not recorded

Answers Antigravity's two newest #618 rounds (no blocking findings).

- turn.ts: handleTurn mints the credential upstream, then records its
  revocation in the budget object. If that call threw (a transient
  Durable Object or storage failure), the request ended in an unhandled
  500 with the admission still charged and a live credential nobody
  would ever revoke. It now fails closed: the credential is withheld,
  the admission refunded, and the client gets a 503 telling it to
  connect directly. Withholding rather than returning was chosen because
  the whole point of the record is that no handed-out credential escapes
  revocation; the minted one is used by no one and lapses at its TTL.
- budget.ts: one alarm could spend up to REVOKE_BATCH x REVOKE_TIMEOUT_MS
  (50 x 5 s = 250 s) on revocation requests against a hung upstream, and
  the object is single-threaded, so admissions would queue behind it.
  The loop now stops after a 15 s wall-clock budget; the keys it did not
  reach are still due, so the alarm's own re-arm (nextRevocation) fires
  again at once.
- to-dos/plans/v3.4.1-plan.md: rows 10b and 10c described the state of
  2026-10-18 (ladders "not waited for", bitstreams "compiling"). 10b now
  records the ladders as cancelled at the maintainer's word, 10c the
  2026-10-19 compiles and their margins. 10e ("after the oracle merges")
  is still true and is unchanged.

Not done, with reasons (replied on the PR):
- bolt job forwarding hosted_base: hosted_base is a workflow_call input
  only, and bolt runs only on workflow_dispatch, where it is always
  empty; bolt's output is never promoted to a release.
- Android validating relay URLs through a shared Rust parser: a UniFFI
  export plus both apps is a v3.4.2-sized change; the regex now refuses
  the empty-host and whitespace cases, and a malformed port still fails
  at connect.
- ra_glue.js reading RA_PROXY_BASE at import: browser RA is held off
  (D31); recorded for when it is enabled.

TURN stays off in production (TURN_ENABLED "false"), so neither Worker
change runs there until it is re-enabled. Verified: vitest 123 passed,
tsc --noEmit clean. No new test: the storage failure and the 15 s
budget both need fault injection or fake timers inside workerd, which
the suite does not have; stated rather than faked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant