mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
v0.2.4.5
2821
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
55cc7218c5 |
fix(i18n): strip unnecessary quote escapes in ko.ts and tr.ts
ESLint no-useless-escape flagged 156 errors (153 in ko.ts, 3 in tr.ts): \" inside single-quoted Korean strings and \' inside double-quoted Turkish strings. The escapes weren't needed because the surrounding quote style differs from the escaped quote. Visible-text unchanged; i18n parity green at 5007 leaves × 10 locales.v0.2.4.5 |
||
|
|
f9c0a69c24 |
Merge pull request #1616 from maziggy/0.2.4.5
# Bambuddy 0.2.4.5 **⚠ Upgrade Notes — Read Before Updating** 0.2.4.5 is a substantial patch release on the same 0.2.4 code base — no schema breaks beyond the documented column additions for the new API-key scopes (auto-migrated on first start, dialect-branched for SQLite and Postgres), no Docker entrypoint changes, no Vite/proxy quirks. The in-app Apply Update button in Settings → System → Updates works for Docker and for any native install already on 0.2.4.1 or later. **Four behavior-change callouts to know about before you upgrade:** - **API-key permission model switched from denylist to allowlist** (security fix). Two new scope flags were added — `can_manage_library` and `can_manage_inventory` — and existing keys that had `can_queue` set are auto-backfilled with both new scopes on first start so library upload and inventory write keep working for keys you'd previously created as "queue-only". A hardened "read-only" key (no `can_queue`) does **not** silently gain new writes on upgrade. Review the Settings → API Keys page after upgrade if you run integrations against specific keys. - **External folder mounting is now disabled by default and requires explicit operator opt-in via `BAMBUDDY_EXTERNAL_ROOTS`** (security fix). If you currently use the File Manager → external folder feature (mounting host paths like a NAS share, USB drive, or `/mnt/library`), you **must** set `BAMBUDDY_EXTERNAL_ROOTS=/path/one:/path/two` in your env / `docker-compose.yml` / systemd unit before upgrading, or all mounted external folders will be rejected on next start. The variable takes a colon-separated allowlist of absolute paths; the mount route is also now gated on the admin `SETTINGS_UPDATE` scope rather than `LIBRARY_UPLOAD` because mounting host paths crosses user boundaries. Bambuddy-owned dirs (data dir, log dir, static dir, archive dir) are hardcode-rejected even if added to the allowlist. See [wiki → Docker → External library folders](https://wiki.bambuddy.cool/getting-started/docker/#external-library-folders-bambuddy_external_roots) for examples. - **Slicer-API sidecar users: bump `BAMBU_VERSION` in `slicer-api/.env` to `02.07.01.57` and rebuild.** If you run the optional OrcaSlicer API sidecar for server-side slicing, the upstream `bambu-studio-api` image switched from the no-longer-published Fedora AppImage to the Ubuntu 22.04 AppImage. The new default in `slicer-api/.env.example` / `slicer-api/docker-compose.yml` matches the new build path. After pulling this release: ``` cd slicer-api docker compose --profile bambu build --no-cache bambu-studio-api docker compose --profile bambu up -d ``` Bambuddy users who don't run the slicer-API sidecar are unaffected. - **Virtual-printer FTP passive port range widened from 50000–50100 (101 ports) to 50000–51000 (1001 ports)** for the non-proxy path. Docker bridge-mode users mapping the old range need to update to `50000-51000:50000-51000`. Docker host-mode and bare-metal users are unaffected. Proxy-mode VPs keep the old range (they pre-bind the printer-side range exactly). Make a backup before upgrading via Settings → Backup → Create Backup. Native install with `update.sh` snapshots the database automatically and rolls back on failure. Docker and fully-manual paths don't. ### Docker ``` docker compose pull docker compose up -d ``` `docker-compose.yml` doesn't need refreshing unless you map the VP passive FTP port range (see above) — no other entrypoint, volume, or env-var conventions changed since 0.2.4. ### Native install — recommended path ``` sudo BRANCH=main /opt/bambuddy/install/update.sh ``` Snapshots the database first and rolls back on failure. ### Native install — manual path ``` sudo systemctl stop bambuddy cd /opt/bambuddy sudo -u bambuddy git fetch --prune --tags --force origin sudo -u bambuddy git checkout main sudo -u bambuddy git reset --hard origin/main sudo /opt/bambuddy/venv/bin/pip install -r requirements.txt sudo systemctl start bambuddy ``` ### Windows install 0.2.4.5 ships Windows installer (#1529). Existing installs upgrade in place via the Service / Update entries on the installer; fresh installs use the new GUI installer end-to-end. --- ## Highlights 0.2.4.5 is a security-led release. The big-ticket security work — API-key permission allowlist, path-traversal hardening across the upload / import / file-write surface with a fifth CI backstop, a WebSocket auth gate and audit-driven sweep, a vitest dev-dep bump, and a PyJWT floor bump for four upstream advisories — sits alongside a Windows-first installer (#1529), Turkish (#1571) and Korean (#1587) locales, OS-aware system theme detection (#1418), and the second wave of virtual-printer hardening (cipher-pin parity on every slicer-facing TLS context #1610, multi-slicer response routing, MQTT brute-force rate-limit, sticky-keys for seven more fields). On the fix side, the biggest reports closed in this cycle: multi-plate archives now report filament / time / cost honestly (#1593), card timestamps render in the browser's local timezone instead of UTC (#1602), the multi-run accuracy badge is suppressed when it would be apples-to-oranges (#1608), the cross-distro TLS handshake regression on hardened-policy hosts is fixed at every VP TLS context (#1610), inventory and AMS handling tightened across transparent filament (#1545), profile-only mismatch popups (#1552), per-spool weight sync (#1530 / #1459), the SpoolBuddy tare banner (#1536), and the OIDC `email` claim auto-provisioning gap (#1569). VP `_pending_files` / temp-file leaks, queue-position assignment, DELETE orphan cleanup, sync-from-db race, and slicer-options cache bounds all landed as one slicer-surface audit bundle (#1558) plus follow-ups. --- ## Security - **API-key permission model rewritten as an allowlist (critical).** The three documented API-key scopes ("Read Status", "Manage Queue", "Control Printer") were enforced only inside the legacy `/api/v1/webhook/*` router; every other route fell through to a 17-entry admin denylist that granted any valid key access to every non-admin endpoint regardless of which scope flags were ticked. Fix: a new explicit mapping pins every non-admin permission to exactly one scope; unmapped permissions return 403 ("administrative operations") regardless of scope. Two new scope flags ship in the same release — `can_manage_library` (gates library upload / update / delete and MakerWorld import) and `can_manage_inventory` (gates inventory create / update / delete and SpoolBuddy kiosk writes). Existing keys with `can_queue` set are auto-backfilled with both new scopes; "read-only" keys don't gain new writes. A new CI test fails the build on any future permission added without a scope classification, so the surface can't silently grow again. See **Upgrade Notes** above for the migration story. - **WebSocket auth gate.** The proactive WebSocket audit caught that `/api/v1/ws` was broadcasting every printer-status / archive / inventory event to anyone who could reach the HTTP port. The endpoint is now auth-gated like the REST surface; events broadcast only to authenticated subscribers. - **Path-traversal hardening across the upload / import / file-write surface.** A private report against `POST /api/v1/projects/import/file` traced two attacker-controlled strings being joined to the library directory with no resolve + containment check (`linked_folders[*].name` from the request `project.json` and per-entry `zf.namelist()` paths from the ZIP itself). Concrete escalation: drop a `.pth` into the venv's `site-packages` for code execution on next restart, overwrite the JWT signing-secret file to forge an admin token, or overwrite `~/.ssh/authorized_keys` on native installs. Fix is structural: a new `safe_join_under(parent, *parts)` helper resolves both sides and asserts containment; both vectors now route through it. Adjacent fixes in the same audit: `GET /api/v1/archives/{id}/photos/{filename}` had no traversal guard and was serving arbitrary paths; `ArchiveService.attach_timelapse` accepted printer-FTP-listing-supplied filenames with `..` segments under the compromised-printer threat model. The audit sweep marked every Path-arithmetic site under `backend/app/api/routes/` AND `backend/app/services/` with an explicit `# SEC-PATH-OK` annotation or routed it through the helper; a new CI test (the fifth security backstop) AST-walks both directories and fails the build on any future unsafe-shape join that's neither helper-routed nor marker-annotated. - **External folder mounting restructured to an opt-in allowlist + admin-only scope.** External folder mounting (host paths like a NAS share or USB drive surfaced into Bambuddy's File Manager) was previously gated on `LIBRARY_UPLOAD` (any non-admin user with that scope) and validated against a small denylist of system directories — meaning anything not on the denylist could be mounted, including the Bambuddy data directory containing other users' archives, the log directory, or arbitrary NFS / SMB mounts. The route now requires the admin `SETTINGS_UPDATE` scope and accepts only paths inside the new `BAMBUDDY_EXTERNAL_ROOTS` env-var allowlist. The default empty allowlist disables the feature outright. Bambuddy-owned directories are hardcode-rejected even if the operator adds them to the allowlist. **This is a breaking change for installs that currently use external folder mounting** — see Upgrade Notes above for the restoration path. - **VP MQTT brute-force rate-limit.** Bambuddy's virtual printer exposes an 8-character access code on the slicer-facing MQTT port; without rate-limiting it was brute-forceable by anyone who could reach the VP's bind IP. A new per-IP sliding-window limiter (5 failures / 60 s) now blocks further CONNECTs from a failing IP for the rest of the window; successful auth clears the IP's failure history. - **VP access codes now compared in constant time.** Both the FTP `PASS` handler and the MQTT CONNECT handler used Python's `==` operator on the 8-character access code. Closes the timing side-channel without changing the protocol surface. - **VP FTP uploads capped at 4 GiB.** `cmd_STOR` now rejects uploads exceeding 4 GiB, deletes the partial file, and replies 426. Without the cap a runaway or malicious client could drive RSS or disk to exhaustion; 4 GiB is well above any realistic multi-plate `.gcode.3mf`. - **vitest bumped 3.2.4 → 4.1.8** (development dependency) to pick up an upstream CVSS 9.8 advisory. No production-runtime impact, but the floor moves so fresh installs and CI pick up the fix. - **PyJWT bumped to >=2.13.0** to pick up four upstream advisories. Audit confirmed Bambuddy's usage is unaffected by every behavioural change in 2.13.0 (HMAC empty-key reject can't trigger, OIDC decode uses raw-key path not PyJWK, `jwks_uri` is HTTPS from discovery, no `b64=false` usage, `enforce_minimum_key_length` not opted into); 229 auth/MFA/OIDC integration tests + 78 auth unit tests pass on 2.13.0; `pip-audit --strict` is clean. - **Trivy DS-0026 silenced on `Dockerfile.test` via `HEALTHCHECK NONE`.** The test image runs `pytest` and exits — no service to probe. Adding a healthcheck would have b een cargo-cult noise. --- ## New Features - **Windows installer (#1529, contributed by @vmhomelab).** Installer for Windows hosts — fresh install, service install / uninstall, in-place update, and a Troubleshooting entry. Existing native Windows users can upgrade via the Update entry on the installer. - **Turkish (`tr`) locale (#1571, contributed by @samedyuksel).** Full Turkish translation across all keys, joining the existing 9-locale set. - **Korean (`ko`) locale (#1587, contributed by @hijae).** Full Korean translation across all keys. - **System theme detection — sidebar toggle and Settings selector follow the OS dark/light preference (#1418, contributed by @TempleClause via PR #1501).** `ThemeMode` gains a third value `system` alongside `dark` and `light`. The provider listens to `window.matchMedia('(prefers-color-scheme: dark)')` and tracks the OS preference in real time. The sidebar toggle now cycles `dark → light → system → dark` with the icon hinting at the next stop; Settings → Appearance gained a 3-button Dark / Light / System selector. Existing users' persisted preference is untouched — anyone on `dark` or `light` stays there and simply gains an extra stop in the cycle. - **MQTT auth rate-limit on the virtual printer.** Per-IP sliding-window limiter (5 failures / 60 s) on VP MQTT CONNECT, brute-force-resistant. See Security section above for the full description. - **Per-slicer MQTT response routing for multi-slicer VP setups.** Pre-fix: when slicer A sent a bridge-forwarded command to a non-proxy VP bound to a target printer, the printer's response was fanned out to every connected slicer. Now responses are routed to the originating slicer only, falling back to broadcast for printer-initiated unsolicited pushes (push_status etc.) and for sequence_ids the routing map never saw. Bounded at 256 entries with FIFO eviction. - **VP child-service readiness barrier.** Each VP child sub-service (FTP, MQTT, Bind, SSDP) now exposes a `ready` event that fires after the socket actually binds; `start_server` awaits all of them with a bounded 5 s timeout. Closes a race where `is_running` reported true while the child sockets were still binding, which a quick poll from the diagnostic route or the VP card could observe. --- ## Changes - **Bug-report template tightened + new Area dropdown.** 170 issues have been closed `invalid` (61 of them in the last 30 days alone — roughly 1 in 5 of all closed issues), nearly always because the reporter hadn't run the in-app diagnostics or checked the documented troubleshooting page. The Connection Diagnostic checkbox, Support Package attachment, and a new "Troubleshooting steps already taken" textarea are now required. The old `Bambuddy / SpoolBuddy / Both` dropdown is replaced with two required dropdowns — `Product` and `Area` (15 options covering the actual feature surface) — and an auto-label workflow applies the matching `area:*` label on every issue open/edit. - **VP virtual-printer FTP server streams uploads straight to disk instead of buffering.** Pre-fix: `cmd_STOR` accumulated every chunk in a list and called `write_bytes` at the end. Peak RSS for a multi-GB `.gcode.3mf` was ~2× the file size and could OOM-kill a low-memory host. The streaming rewrite writes each 64 KiB chunk inline as it arrives, bounding peak memory at one chunk regardless of total upload size. Same change adds the 4 GiB hard cap described above. - **VP virtual-printer FTP passive port range widened from 50000–50100 to 50000–51000.** Docker bridge-mode users mapping the old range need to update — see Upgrade Notes above. - **VP MQTT bridge sticky-keys: 7 more fields preserved across incremental pushes.** Pre-fix: a single 1 Hz incremental push (which only carries changed temps / fan / wifi_signal) wiped any field not in the sticky-keys allowlist. The cached state lost `upgrade_state`, `xcam`, `hw_switch_state`, `nozzle_diameter`, `nozzle_type`, `online`, and `ams_status` after a single tick — BambuStudio's Send pre-flight reads several of these and could refuse Send because the cached push said "unknown firmware state". Same shape as #1228 (storage indicators) and #1558 (live-progress fields). Sticky-keys carry-forward is now also a `copy.deepcopy` (was reference) so a future merge can't corrupt both copies. - **VP target-printer DHCP IP / serial refresh now restarts proxy VPs.** Pre-fix: when a target printer's IP changed (DHCP renewal, network reconfiguration), the running proxy VP kept forwarding to the stale IP forever; the user had to manually toggle the VP to refresh. `sync_from_db` now re-evaluates the proxy target each cycle and restarts the VP when the IP or serial actually changes. - **VP `queue_force_color_match` setting takes effect immediately.** Pre-fix: toggling the per-VP "Force exact color match" setting via the UI silently no-op'd because `sync_from_db`'s "changed" predicate didn't include the field. Now restarts the running instance on toggle. - **VP MQTT client session errors elevated from DEBUG to WARNING.** Production never sees the message at DEBUG; legitimate slicer-side errors (TLS handshake failure, protocol violation) deserve to be visible in the default log level. - **VP MQTT periodic status push: one-line per-minute counter per active slicer connection (#1548 follow-up).** Replaces the noisy per-tick log line with a single per-minute summary, while still surfacing connection-drop events at WARNING. --- ## Fixed **UI / rendering** - Print-run log, spool usage history, camera-token list, and SpoolBuddy device "last calibrated" timestamps now render in the browser's local timezone instead of UTC (#1602, reported by @maziggy, confirmed by @IndividualGhost1905 with a UTC+3 reproduction). Same shape as the #504 timezone-offset bug — four sites the #504 sweep didn't catch because they were added afterwards or were missed. - Archive card's Print Time + accuracy badge are now consistent for multi-run / multi-plate archives (#1608). The card was showing one run's actual duration next to a whole-file estimate, producing badges like "+188%". Multi-run archives now show "Estimated 5h 6m" with no badge; single-run archives keep the badge. - Cancelled prints have their own stats bucket and no longer drag down the Success Rate gauge (#1390 follow-up). - Cancelled bucket icon now uses the semantic warning token (orange) instead of an unrelated palette colour (#1390 follow-up). - Quick Stats: user-cancelled prints now have their own bucket and no longer drag down the Success Rate gauge (#1390 follow-up, reported by @IndividualGhost1905). **Slicer / library** - Sliced `.gcode.3mf` files now render in the 3D preview and expose a Preview-3D action in the file row (#1543, reported by @Vlado-Tarakan). - External-folder `.gcode.3mf` files now show thumbnails, and every ingest path stores the same canonical `file_type` for sliced outputs (#1600, reported by @maziggy). - Deleted local profiles no longer linger in the SliceModal preset dropdown; new manual "Refresh" button surfaces cloud-side deletions without waiting for the 5-minute cache (#1581, reported by @lloydcat). - STL thumbnail noise on first generation: matplotlib cache + font_manager scan no longer log three WARNING lines on first STL upload (reported by @maziggy). - Bulk-upload ZIPs of stub / empty STL files no longer spam the log with thousands of warnings (reported by @maziggy). - Print filenames with FAT32-illegal characters now rejected at rename / upload / queue time instead of failing at FTP (#1540, reported by @anthonyma94). **Archive / stats** - Multi-plate `.gcode.3mf` archives and reprints no longer under-report filament, time, and cost — project stats and parser both fixed (#1593, reported by @needo37). 3-plate file printed plate-by-plate over 9 runs was showing 1/9th of the real material consumption. - Source-3MF upload on "fallback" archives no longer crashes with HTTP 500 (and stops orphaning files outside the data volume) (#1531, reported by @d3nn3s08). - Fallback archives now carry MQTT-derived filament type + colour when the 3MF can't be downloaded (#1533, reported by @JmanB52D), plus a follow-up fix for real prints (#1533 follow-up). **Inventory / Spoolman** - Transparent / clear filament now selectable and rendered as transparent end-to-end in the built-in inventory (#1545, reported by @Synec5, confirmed by @CMW-ISS). - Assigning a spool no longer shows a profile-mismatch warning when only the slicer profile differs; the warning now states the AMS slot will be reconfigured (#1552, reported by @anthonyma94). - External-spool usage is now tracked when the AMS has empty slots in between loaded ones (#1607, reported by @ahmtcnby). - SpoolBuddy weight sync no longer silently lands on a stale local row when Spoolman is enabled (#1530, reported by @chesterakl). - SpoolBuddy Tare status banner no longer sits at "Waiting for device..." forever (#1536, reported by @flom89). **Virtual printer** - Queue / Review / Archive virtual-printer modes now complete the TLS handshake on hardened-distro hosts (#1610). Real Bambu printers offer only the plain-RSA AES-GCM suites `AES256-GCM-SHA384` / `AES128-GCM-SHA256`; a system crypto policy that strips them left the slicer's ClientHello with no overlap. The #620 cipher-suite fix only patched the printer-facing context; this release patches every slicer-facing VP TLS context (bind / MQTT / proxy-server / FTP). - VP "Send file" IP rewrite now also fires for VPs without a dedicated bind IP (#1429 follow-up, residual case confirmed by @Mape6). - VP "Send file" no longer redirects from Bambuddy to the physical printer's SD card once the printer powers on, and the mode button labels now match the wire values stored in the DB (#1429). - VP queue mode no longer blocks BambuStudio Send while the target printer is mid-print (#1558, reported by @phieb). - VP `_pending_files` / temp-file leak fixed on every error path across the three file handlers. - VP queue position now picks `MAX(position)+1` instead of hardcoded `1`; duplicate position=1 entries no longer pile up on non-empty queues. - VP DELETE route now cleans orphan `PendingUpload` rows and the on-disk `upload_dir`; previously these accumulated. - VP `MQTTBridge._refresh_loop` no longer leaks the raw_message_handler on exceptions in the IP-encoding branch. - VP `sync_from_db` serialised by `asyncio.Lock`; concurrent PUT calls (browser racing the auto-save trigger) no longer race the inner start/stop. - VP `_slicer_print_options` cache bounded at 128 entries with FIFO eviction. - VP MQTT bridge sticky-key carry-forward now uses `copy.deepcopy`; a sticky key carried over from the previous cache no longer shares nested dicts with the next push. - VP MQTT no longer drops idle slicer connections at exactly 60 s (#1548, reported by @hollajandro); honours client-negotiated keepalive instead of the hardcoded 60 s. - VP diagnostic now probes both bind ports 3000 (plain) and 3002 (TLS). - VP FTP `stop()` now awaits cancelled sessions instead of `sleep(0.1)`; a session mid-write or mid-TLS-handshake can no longer outlive the stop call. - VP per-VP TLS certificate auto-regenerates when the shared CA is rotated. - VP `tailscale.py::get_status` now catches `asyncio.TimeoutError`; a stuck Tailscale subprocess no longer surfaces an uncaught exception. - VP `certificate.py` CA save uses correct parent directory; previously the CA write could fail because only the per-VP subdirectory had been created. - VP `_extract_plate_id` failures now log at debug instead of swallowing silently; a malformed `Metadata/slice_info.config` is now traceable. **Dispatch / prints** - A1 no longer auto-replays the previous print after a power cycle when the library row's filename has a doubled `.gcode.3mf` (#1542). The dispatch SD-cleanup path now aligns with the upload path; doubled-extension library rows no longer leave ghost prints. - Connected-edge reconciliation closes the missed-PRINT-COMPLETE loop that produced ghost replays on smart-plug power cycles (#1542 follow-up, reported by @vixussrl-ui). - Paused prints no longer inflate maintenance hours (#1521, reported by @TempleClause). `track_printer_runtime` was counting both RUNNING and PAUSE states. - Webhook printer-status / stop / cancel routes 500'd on every connected printer because the route treated the `PrinterState` dataclass as a dict (#1584). **Cloud / auth** - Bambu Cloud sign-in failures caused by an upstream Cloudflare challenge now surface an actionable message instead of "Invalid response from Bambu Cloud" (#1575, reported by @cliveflint). - OIDC auto-provisioning now reads the standard `email` claim for `User.email` when `Email Claim` is set to a non-email identity claim (#1569, reported by @anderl1969). **Notifications** - ntfy notifications: honest User-Agent + actionable error when the server is behind a Cloudflare challenge (#1534, reported by @apizz). User-Agent is now `Bambuddy/<version>` instead of the bare httpx default. **Maintenance** - Custom maintenance type "documentation URL" now persists on create (#1596, reported by @BurntOutHylian — with the exact root cause pre-triaged in the issue body). **Internal / CI** - Path-traversal CI backstop now recognises markers on the closing-paren line (project-wide convention). - Backend test sharded 4-way + `-n auto` for ~3.5× wall-clock speedup in CI; pip cache mount in the test image. - Unit tests no longer re-run inside the test image (duplicated the parent CI job). - Trivy DS-0026 silenced on `Dockerfile.test` via `HEALTHCHECK NONE` (see Security). --- ## Credits External contributors this release: @TempleClause (#1418 / PR #1501 system theme detection), @vmhomelab (PR #1529 Windows installer), @samedyuksel (PR #1571 Turkish locale), and @hijae (PR #1587 Korean locale). Thank you! |
||
|
|
330eb3176c |
Merge remote-tracking branch 'origin/main' into 0.2.4.5
# Conflicts: # CHANGELOG.md # backend/app/core/config.py |
||
|
|
32b3a93e60 |
test(security-fixtures): suppress Bandit B108/B104 on adversarial inputs
The hardcoded /tmp paths and 0.0.0.0 bind in test_archives_api.py / test_attach_timelapse_safe_path.py / test_vp_mqtt_bridge.py are deliberate adversarial-input fixtures for the path-traversal containment tests and the #1429 bind_address=0.0.0.0 auto-resolve path — not insecure temp-file usage by the tests. Same nosec-without-comment pattern as the existing test_virtual_printer.py sites. |
||
|
|
bcd4631012 | Bumped version | ||
|
|
56be4d4647 | Housekeeping | ||
|
|
c36c8cea77 |
chore(deps): bump PyJWT floor to >=2.13.0 for upstream advisories
pip-audit flagged four advisories against 2.12.1, all fixed in 2.13.0. Audited the five behavioural changes in 2.13.0 against our usage; none apply (HMAC empty-key reject can't trigger, OIDC decode uses raw-key path not PyJWK, jwks_uri is HTTPS from discovery, no b64=false usage, enforce_minimum_key_length not opted into). 229 auth/MFA/OIDC integration tests + 78 auth unit tests green on 2.13.0; runtime encode/decode roundtrip verified with the real SECRET_KEY; pip-audit --strict now clean. |
||
|
|
9dc326c892 | Updated README | ||
|
|
fee16f4e34 | Updated README | ||
|
|
2b378a429e | Updated README | ||
|
|
f68eda4da5 | Post work pr #1571 | ||
|
|
d736611ded | feat(i18n): add Turkish (tr) translation (#1571) | ||
|
|
77655d8dfa | feat(i18n): add Korean (ko) translation (#1587) | ||
|
|
0471e43b8a | Housekeeping | ||
|
|
e38267065c |
fix(frontend): #1602 render print-run / spool-usage / camera-token / SpoolBuddy timestamps in local time
Four frontend formatDate / formatDateTime helpers called new Date(iso) directly on backend timestamps that have no timezone indicator. Per ECMAScript, a bare "2026-06-02T07:50:00" is parsed as local time, so a UTC-stored value got displayed as if its numeric components were already local — visually identical to UTC. Same shape as the #504 fix from Feb 2026, which patched 13 sites but missed these four: PrintLogTable and SpoolUsageHistory hadn't been written yet; CameraTokensPage and SpoolBuddySettingsPage existed but were overlooked. Reporter #2 (@IndividualGhost1905) confirmed with a UTC+3 host: print log shows UTC clock value, system date shows local. PrintLogTable is the "logs/completion time" they called out; the other three are the same pattern in nearby surfaces. Replaced the bare new Date(iso) calls with parseUTCDate(iso) from utils/date.ts — the same helper every other date formatter in the codebase already uses. It appends "Z" to naive ISO strings and parses TZ-tagged strings as-is. CameraTokensPage.isExpired got the same fix because comparing a misparsed Date against Date.now() would produce false "not expired" / "expired" results around the TZ-offset boundary. Reporter #1's printer-card ETA complaint (10:50 + 57m showing 09:48) is NOT addressed by this fix. That ETA comes from formatETA(remainingMinutes) which is purely client-side (new Date() plus minutes from the WebSocket payload, then toLocaleTimeString) — for it to render UTC the browser timezone itself would need to be UTC, which is a browser / OS config issue. Audit confirmed no other regressions: grepped every new Date( call in frontend/src/. Remaining sites either pass an epoch-ms number, use the result only for .getTime() arithmetic where the offset cancels, already wrap in parseUTCDate fallback, or consume a backend timestamp that includes "+00:00" (FailureDetectionSettings reads obico_detection.py's tz-aware isoformat). |
||
|
|
cc25cbe774 |
fix(archives): #1608 suppress card time-accuracy badge for multi-run archives
compute_time_accuracy in routes/archives.py compares the archive row's own started_at / completed_at (which reflect the latest run only) against archive.print_time_seconds (which the #1593 parser fix correctly stores as the sum across plates). For a 3-plate file printed plate-by-plate the ratio is ~300%, producing a "+188%" card badge that means nothing — apples to oranges. The 5-500% sanity band catches truly broken values but lets this deterministic N×100% shape through. Reporter's archive #65 was 3 plates over 9 runs. compute_time_accuracy gains an optional run_aggregate argument and returns both actual_time_seconds and time_accuracy as null when the aggregate reports more than one logged run. The frontend already falls through to print_time_seconds for the time display (actual_time_seconds || print_time_seconds) and gates the badge on time_accuracy being truthy, so multi-run archives now show the slicer estimate with no badge. Single-run archives keep the original behaviour verbatim. The fix is applied at every call site that renders an archive card: archive_to_response now threads run_aggregate through, and the three endpoints that previously didn't load the aggregate (archives.py search fast-path and FTS path, single-archive PATCH, and projects.list_project_archives) now batch-load it via the existing _load_run_aggregates helper. The stats endpoint's per-run accuracy aggregation at archives.py:940 already uses PrintLogEntry.duration_seconds with its own 50-200% band filter and is untouched. |
||
|
|
9cc4b6aa60 |
fix(virtual-printer): #1610 add Bambu cipher pin to every slicer-facing TLS context
The #620 patch fixed the OpenSSL-3.x-strips-plain-RSA-AES-GCM cipher mismatch on the printer-facing TLSProxy client context. The same fix was never applied to the four other slicer-facing TLS contexts. On hardened distros (Fedora / RHEL with update-crypto-policies, hardened Alpine builds) where the system narrows DEFAULT to forward-secrecy only, the slicer's ClientHello finds no overlap with what Bambuddy offers and the handshake aborts with the slicer reporting code=-1 before any application data flows. The reporter pinpointed the missing set_ciphers call in bind_server.py against the #620 lineage; the audit-wide sweep here extends the same fix to mqtt_server.py, tcp_proxy._create_server_ssl_context (the missing other half of #620), and ftp_server.py. For the three new contexts (bind / mqtt / proxy-server) the cipher string is DEFAULT:AES256-GCM-SHA384:AES128-GCM-SHA256 — verbatim match with the #620 client-side fix. For FTPS the original HIGH baseline is kept (HIGH:AES256-GCM-SHA384:AES128-GCM-SHA256:!aNULL:!MD5:!RC4) so the cipher set stays a strict superset of what shipped before — HIGH offers ~58 suites DEFAULT doesn't (CCM / ARIA / CAMELLIA / DSS) that no Bambu slicer is known to pick, but narrowing a compat surface without proof would violate the existing don't-remove-compat-pinning rule. TLS version pins (TLSv1_2 minimum across all four, TLSv1_2 max on FTPS for the BambuStudio PSK-reuse compat) and verify-mode settings are unchanged — only the cipher list is widened. |
||
|
|
fee7c722f5 |
fix(usage-tracker): #1607 filter empty AMS slots from position-based fallback
When no explicit slot-to-tray mapping is captured (path 5 of 6 in _track_from_3mf — fires before the request-topic subscription that catches ams_mapping is accepted), the tracker builds available_trays from build_ams_tray_lookup and uses position to map the slicer's Nth filament to the Nth available tray. The helper enumerated every AMS tray by id regardless of whether a spool was loaded, so AMS slots 0-2 loaded + slot 3 empty + external yielded available_trays = [0, 1, 2, 3, 254]. The slicer compacts its filament UI to hide empty AMS slots, so its 4th filament is the external — but position mapping routed it to AMS0-T3 (the empty slot) instead of 254 (external). No spool assigned there → usage silently skipped → external never decremented. Filter the fallback to slots with a non-empty tray_type. build_ams_tray_lookup stays unchanged for its other callers (spoolman_tracking.store_print_data, routes/printers, spool_assignment_notifications); the filter is applied at the usage-tracker call site only. Mirrors the existing vt_tray filter in build_ams_tray_lookup line 174. |
||
|
|
cdc27eb517 |
fix(virtual-printer): #1429 follow-up — auto-resolve VP IP when bind_address is 0.0.0.0
The original #1429 fix's _refresh_ip_encoding early-returned when mqtt_server.bind_address was "0.0.0.0" or empty (the default for VPs created without a dedicated bind IP). On a flat-LAN install that's the typical case, so the encoding never armed, _rewrite_net_info_ips was a no-op on every push, and the slicer kept following the real printer IP to its SD card. @Mape6 reported this on the 2026-06-02 daily that supposedly fixed the bug. New helper _resolve_host_interface_for_target() consults the existing network_utils.find_interface_for_ip() to pick the host interface in the printer's subnet. _refresh_ip_encoding falls back to it when bind_address is unspecified; an explicit bind IP still wins. INFO log line distinguishes the two paths ("armed: ... (bind_address)" vs "(auto-resolved)") so future bundles directly answer which IP the rewrite picked. Tests: 4 new under TestBindAddressAutoResolve — rewrite arms via auto-resolved IP at bind_address=0.0.0.0; stays disabled if no interface matches (no crash); explicit bind_ip still takes precedence; helper returns None defensively when find_interface_for_ip does. |
||
|
|
dd746c1818 | Updated spoolbuddy/install/install.sh | ||
|
|
2a520a1ec7 |
chore(slicer-api): bump BAMBU_VERSION default to 02.07.01.57
Tracks the orca-slicer-api Dockerfile change that switched the sidecar from the (no-longer-published) Fedora AppImage to the Ubuntu 22.04 AppImage. The default in .env.example and slicer-api/docker-compose.yml now matches the new build path; users running the sidecar should `docker compose --profile bambu build --no-cache bambu-studio-api && docker compose --profile bambu up -d` after pulling this change. |
||
|
|
cbbd0bf1cc | Updated BACKERS | ||
|
|
148eab4de6 | Updated BACKERS | ||
|
|
5dfd38597a |
docs(install): polish Windows section + Service/Update/Troubleshooting
entries PR #1529 added the Windows installer but the rendered README sections had a stray blank line inside the install one-liner's code fence and the cross-platform Service Management / Updating / Troubleshooting sections still only covered Linux + macOS + Docker. Fold in Windows entries (Start-Service, Get-NetTCPConnection, NSSM runtime log path) and link the README description to the Windows Installer wiki page so users know where the parameter reference and unattended examples live. |
||
|
|
bd19a8ccb4 |
docs(install): polish Windows section + Service/Update/Troubleshooting
entries PR #1529 added the Windows installer but the rendered README sections had a stray blank line inside the install one-liner's code fence and the cross-platform Service Management / Updating / Troubleshooting sections still only covered Linux + macOS + Docker. Fold in Windows entries (Start-Service, Get-NetTCPConnection, NSSM runtime log path) and link the README description to the Windows Installer wiki page so users know where the parameter reference and unattended examples live. |
||
|
|
4582a28866 |
Feature/windows installer (#1529)
Add feature: Added a powershells script to easily install bambuddy on windows |
||
|
|
873c78a6ff |
Feature/windows installer (#1529)
Add feature: Added a powershells script to easily install bambuddy on windows |
||
|
|
673001e3cd |
fix(maintenance): #1596 persist wiki_url on Custom Type create + correct
inline mutation type
POST /api/v1/maintenance/types hard-coded the MaintenanceType
constructor and silently dropped `wiki_url`, so the Documentation URL
field disappeared after save. PATCH worked because it uses
`data.model_dump(exclude_unset=True) + setattr`, which is why editing
a freshly-created type DID save the URL — masking the bug under any
"save then immediately fix it" retest. Reporter @BurntOutHylian
pre-triaged the issue to the exact constructor call at
routes/maintenance.py:206-213; fix is the missing `wiki_url=data.wiki_url`
argument.
Frontend nit from the same report: MaintenancePage.tsx:1131's
`updateTypeMutation` declared `data: Partial<{ name; default_interval_hours;
interval_type; icon }>` — omitting `wiki_url`. The value reached the
API correctly at runtime because `api.updateMaintenanceType` accepts
`Partial<MaintenanceTypeCreate>` (which has wiki_url), but the inline
type lied about the payload shape. Extended the inline `Partial<{...}>`
to include `wiki_url?: string | null`. Pure type fix — no runtime change.
|
||
|
|
a837a3acbd |
● fix(library): #1600 thumbnail extraction for external-folder .gcode.3mf
files + unify file_type classification across ingest paths #1600: external-folder sliced outputs landed with no thumbnail. Cause: four backend ingest paths classified LibraryFile.file_type differently for the same .gcode.3mf family. upload / ZIP-extract / in-process used os.path.splitext()[1] which returns .3mf for foo.gcode.3mf and stored file_type="3mf", matching the thumbnail-extraction gate at library.py:1467 (file_type == "3mf"). External-folder scan explicitly detected the compound and stored file_type="gcode.3mf" — preserving "sliced output" identity — but then skipped both the "3mf" gate and the "gcode" gate, so the file landed with thumbnail_path = None. Same compound-extension drift that bit #1543 in the 3D preview, in a surface that audit didn't trace back to. Unified fix: - New classify_file_type(filename) helper in routes/library.py is the single source of truth. Returns "gcode.3mf" for sliced outputs and ext[1:] otherwise. - Applied to every ingest path: upload (line 1704), ZIP-extract (1998), external-folder scan (the bug site — the manual compound check is replaced), and in-process save_3mf_from_bytes (471, used by MakerWorld import). - External-scan thumbnail gate widened to `if file_type in ("3mf", "gcode.3mf"):` — a .gcode.3mf IS a 3MF zip with Metadata/plate_1.png; ThreeMFParser doesn't care about the trailing extension. - gcode-download endpoint at GET /library/files/{id}/gcode had the same drift in reverse: gate was `elif file.file_type == "3mf":` so a row stored with file_type="gcode.3mf" (the external-scan path's pre-unification behaviour, and the canonical going forward) got rejected with HTTP 400. Widened to the same compound-aware tuple. One-shot DB migration in core/database.py::run_migrations backfills existing legacy rows: UPDATE library_files SET file_type = 'gcode.3mf' WHERE file_type = '3mf' AND LOWER(filename) LIKE '%.gcode.3mf' Idempotent (post-update rows no longer match the file_type='3mf' predicate, so re-runs at every boot are no-ops) and dialect-neutral (LOWER + LIKE are identical under SQLite and Postgres). Without the backfill, users would have a permanent split state: old uploads at '3mf', new uploads at 'gcode.3mf' — which would double-bucket sliced outputs in the dashboard stats query at line 4615 and show two entries in the file-manager filter dropdown for the same conceptual type. Frontend untouched. FileManagerPage.tsx and ProjectDetailPage.tsx already accept both '3mf' and 'gcode.3mf' per the #1543 fix. After the migration the DB only contains canonical values, so the legacy '3mf' branches in the frontend become dead code for sliced files — they stay as defence-in-depth in case any future ingest path I missed reverts to the legacy classifier. |
||
|
|
5d6d928b3f |
fix(virtual-printer): #1429 net.info[*].ip cache leak + mode wire-value rename
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy. |
||
|
|
1e08c25a9f |
fix(stats): #1593 multi-plate parser + per-run project rollup + carry-over system totals + accuracy band
Two stacked causes under-reported multi-plate prints in the project
rollup and the archive card.
Root cause 1 - parser only read plate 1.
ThreeMFParser._parse_slice_info used root.find(".//plate") and pulled
prediction / weight from that one element. Any multi-plate file's
archive-level print_time_seconds / filament_used_grams reflected
plate 1 alone. The /plates endpoint already looped findall and was
correct, which is why the plate carousel showed the right numbers
while the archive card was wrong.
Fix: loop every <plate> and sum prediction + weight. Per-plate
concepts (plate_number, _plate_index, printable_objects) only set
when there's exactly one plate - for multi-plate exports the
archive represents all plates and a single index doesn't apply at
the file level. bed_type keeps the first plate's value as a
best-effort default. Malformed prediction / weight on individual
plates skip cleanly rather than poison the sum.
Root cause 2 - project rollup aggregated PrintArchive, not the
per-run log.
compute_project_stats and list_projects quick-stats summed
PrintArchive.print_time_seconds / filament_used_grams / cost /
energy_* WHERE project_id. A reprint reuses the source archive row
and writes a new PrintLogEntry, so 3 sequential runs collapsed to 1
archive - and that archive's numbers were already plate-1-only from
cause 1. The Archive Print Log path was already correct because it
drove off print_log_entries (archives.py:420 comment).
Fix: both compute_project_stats and the list_projects quick-stats
block inner-join print_log_entries -> print_archives WHERE
archives.project_id. total_archives becomes COUNT(PrintLogEntry.id),
failed_prints counts runs in failed/aborted/cancelled/stopped,
completed_items is SUM(PrintArchive.quantity) for runs where
status='completed', time/filament/cost/energy from PrintLogEntry.
Orphan log rows (archive_id IS NULL post archive deletion) are
excluded by the inner join.
Same-shape fixes carried forward (no follow-ups per project rule):
system.py system-info totals: total_print_time / total_filament
had the same bug shape - summed PrintArchive directly so reprints
collapsed to one row. Now sums PrintLogEntry.duration_seconds /
filament_used_grams. The semantic shift is also a correctness
improvement: the field now reflects time the printer actually spent
printing, not slicer-estimated time.
archives.py time-accuracy metric: estimate / actual per run where
estimate = PrintArchive.print_time_seconds. Post-parser-fix
multi-plate archives have file-level estimate but per-run actual =
one plate, so ratio = N x 100% for an N-plate file. The calc now
clamps each row to the [50%, 200%] plausibility band before
contributing to the printer-level average; single-plate accuracy
(the case the metric is designed for) stays fully included.
Backfill: users with AMS spool tracking - the reporter's case - have
per-run filament_used_grams from the tracked spool delta, so stats
become correct immediately. Users without tracking fall back to the
archive estimate and undercount until they reprint. Archive card
still reads PrintArchive.filament_used_grams directly so old
multi-plate archives keep plate-1-only numbers until reslice -
forward-only as the reporter accepted.
|
||
|
|
c9bc5eb4e9 |
fix(webhook): #1584 PrinterState dataclass attribute access in status / stop / cancel
webhook.py treated printer_manager.get_status() return as a dict and
called .get(...) on it. The return is a PrinterState dataclass
(backend/app/services/bambu_mqtt.py), so the call raised AttributeError
and Starlette surfaced it as a generic 500 for every printer with a
status row. Non-existent printers correctly returned 404 because the
early "Printer not found" branch fired before the crash.
Reporter's repro matched exactly: id 1 (existing printer) returned 500,
id 2 and id 3 (no row) returned 404. Verified end-to-end against a live
PG-backed instance with the reporter's key shape — same 500 before the
patch, 200 with the correct payload after.
8 crash sites across 3 routes:
- webhook_get_printer_status GET /printer/{id}/status 5 sites
- webhook_stop_print POST /printer/{id}/stop 2 sites
- webhook_cancel_print POST /printer/{id}/cancel 2 sites
Every status.get("X", default) replaced with status.X if status else
default. Pydantic response schema unchanged; PrinterState's dataclass
defaults cleanly cover the "registered but never connected" branch so
the status route now returns 200 with connected=false, state=null
rather than crashing.
|
||
|
|
171848a0fc |
fix(slicer-presets): #1581 SliceModal refresh + invalidate on local-profile delete/import
Two-part fix for the reporter's "removed profiles still show on the slice
menu" symptom.
Local half (real bug). LocalProfilesView's import and delete mutations
invalidated ['localPresets'] (the management view's own query) but not
['slicerPresets'] (the SliceModal's unified preset query, staleTime 60s).
A freshly-deleted preset kept rendering in the slice dropdown until that
staleTime elapsed plus a refocus/remount. Both mutations now also call
queryClient.invalidateQueries({queryKey: ['slicerPresets']}).
Cloud half (opt-in cache bypass). _fetch_cloud_presets keeps a 5-minute
per-(user, token) in-process cache (slicer_presets.py:69, balances
"users see freshly-saved presets quickly" against "busy install doesn't
hit Bambu Cloud once per modal open"). Users delete cloud presets in
Bambu Studio / Bambu Handy, not in Bambuddy, so there's no event hook
to invalidate on. Rather than shorten the TTL globally, the listing
endpoint gains an opt-in ?refresh=true query param that bypasses both
the cloud cache AND the 1-hour bundled-preset cache for that one call;
the fresh result is still written back so subsequent normal callers
keep hitting the cache.
New SliceModal "Refresh" button. Lives in the preset section header
next to the cloud-status banner. Calls getSlicerPresets({refresh: true})
and writes the fresh slots into the ['slicerPresets'] cache via
queryClient.setQueryData so the spinner stops immediately rather than
triggering a second refetch. RefreshCw icon spins while in-flight;
disabled during slice enqueue to prevent double-fire.
|
||
|
|
396e9aa09e |
security: harden path-traversal class across routes + services; fifth CI backstop
Two attacker-controlled strings were being joined to library_dir with no
resolve + containment check in the project ZIP import endpoint:
- linked_folders[*].name from the request's project.json
- per-entry zf.namelist() paths from the ZIP itself
An absolute path in either field collapsed the join (Path("/lib") / "/etc"
becomes Path("/etc") because pathlib discards the left side when the right
is absolute) and the next write_bytes landed wherever the attacker chose.
Adjacent finding from the routes audit: GET /archives/{id}/photos/{filename}
had NO validation on filename and FileResponse-served arbitrary paths -
the DELETE counterpart at least gated on the photos membership check.
Adjacent finding from the services audit: ArchiveService.attach_timelapse
wrote archive_dir / filename where filename ultimately came from a printer's
FTP listing (compromised-printer threat model) or the /timelapse/select
query param. A malicious printer that exposes a directory entry with ..
segments could write the timelapse outside the archive directory.
New backend/app/utils/safe_path.py::safe_join_under(parent, *parts) is the
single source of truth: rejects empty / null-byte / absolute parts up-front,
joins under parent, resolves both sides, asserts is_relative_to. Returns the
resolved canonical path on success, raises HTTPException(400) on escape, or
PathTraversalError when http=False (for service-layer callers that need to
match a non-HTTP return contract).
Wired into the import vectors, both archive photo handlers, and the
attach_timelapse service. The full audit sweep inspected every Path/Name
join in backend/app/api/routes/ AND backend/app/services/ - 25 route-layer
sites + 8 service-layer sites confirmed safe and tagged with
# SEC-PATH-OK: <reason> so future audits trust the inline guard at a glance.
Fifth CI backstop test_route_path_arithmetic_is_safe_joined_or_marked
AST-walks both layers and fails the build on any <dir-like>/<bare variable>
join that doesn't either route through safe_join_under or carry the marker.
The services layer is in scope because it receives values verbatim from the
routes AND from external sources Bambuddy has no control over (the printer
FTP-listing case above).
SECURITY.md gets a fifth rule + a fifth row in the CI test mapping table;
the rule now names the printer FTP-listing case explicitly so future
services-layer audits set the right expectation.
--------------
fix(library): suppress warning storm when bulk-uploading ZIPs of empty/stub STL files
Uploading a ZIP of stub or empty STL files (e.g. the 24-byte
"solid test\nendsolid test" shape) produced one WARNING per file in
stl_thumbnail.py::generate_stl_thumbnail. The warnings were technically
correct - trimesh returns a valid Mesh with zero vertices, the safeguard
matches, and the function returns None so the library entry is still
created without a thumbnail - but the volume turned a successful upload
into thousands of WARNING lines in the journal.
Two changes:
1. The per-file "Failed to load STL or empty mesh" message in
stl_thumbnail.py is now logger.debug instead of logger.warning. It's
a per-file content observation, not an actionable error; the caller
already handles None correctly. The branch now catches the rare
"large enough but trimesh still can't parse it" case, visible in
debug logs without spamming production.
2. New module constant MIN_USABLE_STL_BYTES = 200 (smallest binary STL
with one triangle is 134B, smallest ASCII ~150B; 200 is a safe floor
below any real STL). The three thumbnail call sites in library.py
(extract_zip_file, single-file upload, _backfill_external_stl_thumbnails)
pre-skip files below this size before calling generate_stl_thumbnail.
Stubs never enter the trimesh pipeline at all.
Behavior is unchanged for real STLs: any file >=200 bytes runs through
the existing pipeline, MAX_VERTICES still triggers simplification at
100k vertices for the 256x256 thumbnail render, large files still get
thumbnails.
------------
fix(stl-thumbnail): silence matplotlib first-import noise (writable cache + font_manager log level)
On first STL upload, three matplotlib-internal log lines surfaced:
WARNING [matplotlib] /opt/claude/.config/matplotlib is not a writable directory
INFO [matplotlib.font_manager] Failed to extract font properties from NotoColorEmoji.ttf
INFO [matplotlib.font_manager] generated new fontManager
The writable-dir warning fired because Bambuddy's $HOME isn't writable for
matplotlib's default config path; matplotlib fell back to /tmp/matplotlib-XXX
which lost the font cache on every host reboot, so font_manager rebuilt it
each cold start - producing another batch of INFO lines.
Fix is two small additions in stl_thumbnail.py before the matplotlib import:
1. New _configure_matplotlib_cache() sets MPLCONFIGDIR to
settings.base_dir/.cache/matplotlib (mkdir if missing) so the cache
persists across container restarts and the writable-dir warning never
fires. Respects an externally-set MPLCONFIGDIR so operators who chose
their own path aren't overridden. Best-effort with a debug fallback if
settings can't be imported or the mkdir fails.
2. logging.getLogger("matplotlib.font_manager").setLevel(WARNING) at module
import demotes the per-font INFO scan that fires when font_manager
builds its cache cold. Real font warnings (>= WARNING) still surface.
3 new tests: font_manager logger at WARNING after module import;
_configure_matplotlib_cache creates the directory under base_dir and sets
MPLCONFIGDIR; an externally-set MPLCONFIGDIR is preserved verbatim.
5516 backend tests green, frontend gates clean.
|
||
|
|
db9b20631c |
fix(cloud): #1575 surface actionable error when Bambu Cloud Cloudflare challenge swallows the JSON response
When Cloudflare in front of bambulab.com returns a "Just a moment..." interstitial instead of the JSON the API normally produces, the parse error in verify_totp / verify_code / login_request used to surface as the opaque "Invalid response from Bambu Cloud" or a generic 401 from BambuCloudAuthError. Reporter hit this with three back-to-back TOTP attempts; a curl from a different network with the same honest Bambuddy UA returns clean JSON, so the trigger is CF-side (per-IP / TLS-fingerprint / rate / transient mitigation), not our code. Add a small _detect_cloudflare_challenge() helper that inspects the response for four CF markers (body "Just a moment...", body "challenges.cloudflare.com", 403 with cf-mitigated header, 503 with cf-ray header) and returns a message that attributes the block to Bambu Lab's Cloudflare protection, suggests waiting a few minutes, and points the user at a same-network browser sign-in as the standard workaround. Wired into all three JSON-parse sites; verify_totp previously had a defensive catch, login_request and verify_code now do too. No header changes, no impersonation, no retry loop - pure diagnostics. Stays clearly on the right side of Bambu Lab's "no falsified client identity" line. |
||
|
|
be15a375a6 |
fix(oidc): #1569 populate User.email from standard 'email' claim when email_claim is preferred_username
When an operator configures `Email Claim = preferred_username` (e.g. Authentik) the primary `_resolve_provider_email` correctly rejects the identity value as non-email shaped and returns None, leaving auto-provisioned users with `email=None` even though the same token carries a valid standard `email` claim. Add a narrow fallback in the auto-create-users branch only: when `provider.email_claim != "email"` and the primary returned None, resolve the standard `email` claim with the same Fall A/B shape + email_verified enforcement and use it for `User.email` and `UserOIDCLink.provider_email`. The auto-link-existing-accounts gate is left on the primary `provider_email`, so the GHSA Fall-B / Fall-C guards remain intact - the fallback never feeds account matching. |
||
|
|
b7d7c82501 | fix(security): WebSocket auth gate + audit-driven hardening sweep | ||
|
|
9c8df1744d |
chore(deps): bump vitest 3.2.4 → 4.1.8 (GHSA-5xrq-8626-4rwp, CVSS 9.8)
The Vitest UI server's /__vitest_attachment__ handler bypasses
isFileServingAllowed via a path-traversal payload, allowing arbitrary file
read/execute on the host. Dev-scope only and not exploitable in
Bambuddy's CI/CLI usage (we don't start the Vitest UI server and
@vitest/ui is not installed), but bumping clears the Dependabot alert
and brings us onto the supported 4.x line.
Bumped:
vitest 3.2.4 → 4.1.8
@vitest/coverage-v8 3.2.4 → 4.1.8
Migration-required fix:
StreamOverlayPage.test.tsx mocked `WebSocket` via
vi.stubGlobal('WebSocket', vi.fn().mockImplementation(() => ({...})))
and the page does `new WebSocket(url)`. Vitest 4 dropped support for
arrow-function constructor mocks ("is not a constructor"). Rewrote
with a plain `function` so `new` resolves correctly.
All 2043 frontend tests pass; npm run build clean; npm audit shows 0
vulnerabilities.
|
||
|
|
ec51394196 |
fix(security): GHSA-r2qv-8222-hqg3 — allowlist API-key permissions (CVSS 9.9)
API-key permission gates went from a 17-entry admin denylist with the three
documented scope flags (can_read_status / can_queue / can_control_printer)
enforced only inside /api/v1/webhook/* to an explicit per-Permission
allowlist consulted by every dependency:
- core/auth.py: _APIKEY_SCOPE_BY_PERMISSION maps every non-admin
Permission to one scope flag on APIKey; unmapped = 403.
_check_apikey_permissions now takes the api_key and checks the flag.
- require_any_permission_if_auth_enabled + require_ownership_permission
were returning None for any valid key with zero scope check; both now
invoke _check_apikey_permissions and fail closed.
- Two new scope flags on api_keys: can_manage_library (LIBRARY_UPLOAD /
UPDATE_OWN / DELETE_OWN / MAKERWORLD_IMPORT) and can_manage_inventory
(INVENTORY_CREATE / UPDATE / DELETE / FORECAST_WRITE — required by
SpoolBuddy kiosks). Default TRUE, backfilled from can_queue so existing
"queue-only" keys keep working and hardened "read-only" keys do not
silently gain writes.
- CLOUD_AUTH now routed through can_access_cloud for defence-in-depth
alongside the existing _cloud_api_key_gate.
- Migration column-existence check (_api_keys_column_exists) gates the
backfill so user-edited values are never overwritten on restart.
Structural drift backstop: test_every_permission_has_a_classification fails
CI on any new Permission added without an explicit scope mapping —
prevents the denylist-shape regression that grew the prior surface.
Backend 5469 tests green; ruff clean. Frontend build green; i18n parity
green across 9 locales (5005 leaves each, +6 new keys). Wiki permissions
table + allowlist callout + upgrade notes updated.
|
||
|
|
5a93b33fd1 | Housekeeping | ||
|
|
7fa58d8689 | Housekeeping | ||
|
|
4ffefa60f4 |
chore(vp): per-minute MQTT status-push counter for idle-disconnect triage (#1548 follow-up)
The 1 Hz status push was silent at INFO, so support bundles couldn't show
whether the push task was actually reaching a specific slicer connection.
Now ``_periodic_status_push`` emits one line per minute per connected
slicer ("1Hz status push: N pushes/min to <client>") and stays silent when
no slicer is attached. No behaviour change to the push itself — counters
are local to the task and reset every 60 ticks.
Motivated by the open follow-up on #1548: keepalive parser shipped (
|
||
|
|
f6f6f92e38 | test(pytest): silence upstream starlette httpx2 deprecation noise | ||
|
|
0967f9758b | test(pytest): silence upstream starlette httpx2 deprecation noise v0.2.4.4 | ||
|
|
c2ab78a69e |
Merge remote-tracking branch 'origin/0.2.4.4' into resolve-pr-1566
# Conflicts: # .github/workflows/ci.yml # Dockerfile.test |
||
|
|
b6fa77a649 | Bumped version | ||
|
|
845ad39b19 |
fix(security): GHSA-6mf4-q26m-47pv — fail-closed on auth-probe DB errors (CVSS 9.8)
is_auth_enabled() and auth_middleware both caught every exception during
the auth-state probe and returned the "allow" answer instead of denying
the request. Reporter's PoC floods /api/v1/auth/login to exhaust file
descriptors, forcing the next SQLite connect to raise, then hits a
protected endpoint during the fail-open window with no token — granting
unauthenticated access to admin-account creation, API-key creation, DB
backup download, and printer control. CWE-636 / CWE-755. Affects >= 0.1.6.
Fix:
- is_auth_enabled (backend/app/core/auth.py): only returns False for the
legitimate "settings row absent" case; any actual exception propagates
so the caller can deny the request.
- auth_middleware (backend/app/main.py): returns 503 on any probe failure
instead of await call_next(request).
4 new regression tests in test_auth_fail_closed.py pin the contract
(propagates DB exceptions, returns False for no-row, True for "true",
False for "false"). 1 existing security test renamed and updated to
accept either 500 or 503 (both fail-closed) and to verify the
SQLAlchemy detail does not leak in the body.
Codebase grep confirmed no other auth-decision predicate has the same
fail-open shape: _validate_api_key returns None on catch (→ 401 fail-
closed downstream), is_advanced_auth_enabled propagates correctly,
permissions.py has no catch-alls.
Reported by @wondercrash via private advisory.
|
||
|
|
e10462678f |
fix(security): GHSA-6mf4-q26m-47pv — fail-closed on auth-probe DB errors (CVSS 9.8)
is_auth_enabled() and auth_middleware both caught every exception during
the auth-state probe and returned the "allow" answer instead of denying
the request. Reporter's PoC floods /api/v1/auth/login to exhaust file
descriptors, forcing the next SQLite connect to raise, then hits a
protected endpoint during the fail-open window with no token — granting
unauthenticated access to admin-account creation, API-key creation, DB
backup download, and printer control. CWE-636 / CWE-755. Affects >= 0.1.6.
Fix:
- is_auth_enabled (backend/app/core/auth.py): only returns False for the
legitimate "settings row absent" case; any actual exception propagates
so the caller can deny the request.
- auth_middleware (backend/app/main.py): returns 503 on any probe failure
instead of await call_next(request).
4 new regression tests in test_auth_fail_closed.py pin the contract
(propagates DB exceptions, returns False for no-row, True for "true",
False for "false"). 1 existing security test renamed and updated to
accept either 500 or 503 (both fail-closed) and to verify the
SQLAlchemy detail does not leak in the body.
Codebase grep confirmed no other auth-decision predicate has the same
fail-open shape: _validate_api_key returns None on catch (→ 401 fail-
closed downstream), is_advanced_auth_enabled propagates correctly,
permissions.py has no catch-alls.
Reported by @wondercrash via private advisory.
|
||
|
|
597762685c |
fix(virtual-printer): #1558 Send pre-flight + slicer-surface audit bundle
#1558: cached-as-base push_status only forced gcode_state=IDLE while letting the real printer's live-progress fields (mc_percent, stg_cur, layer_num, ...) leak through. Bambu Studio's Send pre-flight read them as busy and refused. The cached branch now overrides the activity-field set the same way it already overrode storage indicators (#1228) and protocol fields. Same bundle ships a multi-round VP audit that found adjacent bugs in the same family: - #1558: cached branch zeroes mc_print_stage / mc_percent / mc_remaining_time / stg / stg_cur / layer_num / total_layer_num / print_error - MQTT auth: per-IP rate-limit (5/60s lockout), hmac.compare_digest, access_code redacted in DEBUG log - FTP cmd_STOR streams chunks to disk + 4 GiB cap (was buffering whole upload) - Sticky-keys allowlist extended with upgrade_state / xcam / hw_switch_state / nozzle_diameter / nozzle_type / online / ams_status - _pending_files cleanup in finally for archive / queue / dispatch handlers - _add_to_print_queue position uses MAX+1 (was hardcoded 1) - DELETE VP removes orphan PendingUpload rows + upload_dir from disk - Per-VP cert regenerates on shared-CA rotation (real signature verification, not DN match) - DHCP target-IP refresh + queue_force_color_match toggle now restart proxy VPs - Per-slicer bridge-response routing (multi-slicer cross-leak fix via sequence_id map) - Child-service readiness barrier (FTP / MQTT / Bind / SSDP) — no false is_running before sockets bind - H2D Pro O1E / O2D model codes added (experimental, needs field confirmation) - FTP passive port range widened 50000-51000; docker-compose + wiki updated - VP refresh_loop crash now unbinds raw_message_handler; tailscale catches asyncio.TimeoutError; SlicerProxyManager lifecycle hardening |
||
|
|
40cd45e2d5 |
fix(library): render 3D preview for sliced .gcode.3mf files (#1543)
Reporter exported a multi-plate .gcode.3mf from Bambu Studio to the
shared folder Bambuddy watches and the 3D preview tab came up empty;
if he re-uploaded the same file via the file manager, the preview
worked. Root cause: two paths classify file_type differently. The
shared-folder scan at backend/app/api/routes/library.py:1343-1348
does a compound-extension check and tags the file `gcode.3mf`; the
upload path at the same file's :1588 does a single `ext[1:]` and tags
it `3mf`. Then frontend/src/components/ModelViewerModal.tsx:71-73 had
hasModel = normalizedType === '3mf' || 'stl'
hasGcode = normalizedType === 'gcode' || '3mf'
Neither matched `gcode.3mf`, so the capabilities object landed with
both flags false and the modal rendered an empty bed.
FileManagerPage.tsx:858 also gated the Preview-3D context action on
`file_type === '3mf' || 'gcode' || 'stl'`, so for shared-folder files
the menu entry didn't even appear, and the type pill at :765-770 had
no colour case for `gcode.3mf` so it fell through to the generic
gray.
Fix (frontend-only, no backend churn):
- ModelViewerModal.tsx introduces an
`isThreeMfFamily = normalizedType === '3mf' || normalizedType === 'gcode.3mf'`
predicate used in two branches — the capabilities check
(`hasModel = isThreeMfFamily || 'stl'`, `hasGcode = isThreeMfFamily
|| 'gcode'`) and the plates-loading branch that previously hard-
gated on `!== '3mf'` and would have returned setPlatesData(null)
for the shared-folder file.
- FileManagerPage.tsx adds `gcode.3mf` to the Preview-3D action gate
and shares the gcode blue type-pill colour so sliced-output files
are visually distinguishable from source 3MFs.
The compound `gcode.3mf` classification on the backend is intentionally
preserved — it carries useful "this is a sliced output" semantics that
other UI surfaces could use later. The `canOpenInSlicer` and
`sliceableType` checks at ModelViewerModal.tsx:269, 277-280 are
deliberately left alone — a sliced output isn't openable in the slicer,
and `sliceableType` already explicitly excludes `.gcode` and
`.gcode.3mf` per the comment.
Out of scope (separate Bambu-Studio format limitation, not a Bambuddy
bug): Vlado's secondary observation that the upload-path 3D preview
"shows only one plate" even though his project has 5 plates — Bambu
Studio's .gcode.3mf export contains the model data and g-code for the
active plate only, not the entire multi-plate project. The print
picker enumerates plates via gcode_*.gcode entries inside the zip
(a separate code path), which is why the user can still pick the
plate at print time. The empty-bed fix is the data point that closes
the user-visible bug.
|