From b6d2b7fdca32cb3780f865ff4221ccfa21cea6a6 Mon Sep 17 00:00:00 2001 From: saime <2286263079@qq.com> Date: Mon, 27 Jul 2026 03:42:55 -1000 Subject: [PATCH] chore: release v0.2.1 v0.2.0 was tagged and published at 07:48Z on 2026-07-26; the audit fixes merged at 17:10Z the same day. The published PyPI package and GHCR image therefore ship without them, including the one high-severity finding: a non-loopback bind had no authentication at all. v0.2.1 exists to get those fixes to anyone who installed v0.2.0. No code changes beyond the version bump. CHANGELOG documents what e596a04 actually changed, which that commit did not record. create_app reads __version__ instead of repeating the literal, so a release bumps four files rather than five. --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/dreamloop/__init__.py | 2 +- src/dreamloop/web.py | 3 ++- tests/test_packaging_release.py | 2 +- uv.lock | 4 ++-- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b08123..202c8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ ## Unreleased +## v0.2.1 - 2026-07-27 + +A six-dimension audit of v0.2.0 (architecture, code quality, security, frontend, tests, docs) produced 31 findings. The fixes landed after the v0.2.0 release was cut, so **v0.2.0 does not contain them** — upgrade if you run DreamLoop anywhere but loopback. + +### Security + +- Binding a non-loopback host (`--host 0.0.0.0`, the Docker demo) now enables token authentication, prints the access URL at startup, and accepts the token via cookie, header, or query. Previously the app had no authentication at all and relied on a spoofable `Host` header, so a published Docker port exposed every dream to the local network and let anyone repoint the AI provider. +- Refuse header-less writes from non-loopback clients unless they authenticate. `--no-auth` / `DREAMLOOP_NO_AUTH=1` keeps them open, because that mode has no token to offer a scripted client. +- Hand `?token=` to a cookie and redirect it out of page URLs, and redact it from uvicorn's access log, which records full query strings. +- Compare tokens as bytes: `compare_digest` raises `TypeError` on non-ASCII input, which turned a bad token into a 500. +- Return JSON rather than HTML for 401s on `/api/` paths. + +### Data safety + +- `migrate_analysis_table` dropped the renamed old table unconditionally, so an analysis table missing a required column lost every stored analysis silently. Unmigratable rows are now preserved in a backup table, and the migration paths have test coverage. +- Recover from a corrupt `config.json` instead of returning 500 from every page, including the settings page needed to fix it. Both config writers now replace the file atomically. +- Wrap invalid model JSON as `AnalysisIncomplete`; a truncated or fenced response from a local model surfaced as a 500. + +### Fixes + +- `POST /drafts/analyze` stores the draft and redirects, so a refresh no longer re-runs a slow LLM call and the URL survives navigation. +- Log the three swallowed exceptions in `web.py` (the project had no logging at all), stop reporting a missing dream as a provider error, and surface image-generation failures to the user. +- Map import errors to 400/404 instead of 500. +- `init()` is idempotent; every `DreamLoop` method re-ran the full migration. +- `feedback_summary` uses one join instead of a query per feedback row. +- Clamp dream text in cards, fix heat-cell contrast to WCAG AA, add a skip link, reset loading buttons on bfcache restore, and preload the bundled fonts. +- Add `SECURITY.md`, issue and PR templates, and document that `.dreamloop/` is created relative to the working directory. +- 212 tests pass; coverage 87% -> 91%. + ## v0.2.0 - 2026-07-25 - Upgrade all six web pages with a shared local design system, page-specific atmospheric art, progressive reveal and count-up effects, native dream-card transitions, responsive navigation, and refreshed English/Chinese screenshots. diff --git a/pyproject.toml b/pyproject.toml index 0212d1e..ca3f93d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dreamloop" -version = "0.2.0" +version = "0.2.1" description = "A local-first AI dream journal for people who want to notice patterns in their dreams." readme = "README.md" requires-python = ">=3.11" diff --git a/src/dreamloop/__init__.py b/src/dreamloop/__init__.py index 87d1314..8cead5a 100644 --- a/src/dreamloop/__init__.py +++ b/src/dreamloop/__init__.py @@ -1,3 +1,3 @@ """DreamLoop local-first dream journal.""" -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/dreamloop/web.py b/src/dreamloop/web.py index 8f3abb6..dda9974 100644 --- a/src/dreamloop/web.py +++ b/src/dreamloop/web.py @@ -16,6 +16,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from . import __version__ from .analysis import ( AnalysisIncomplete, AnalysisLanguageMismatch, @@ -707,7 +708,7 @@ def create_app( auth_token: str | None = None, allow_unauthenticated_writes: bool = False, ) -> FastAPI: - app = FastAPI(title="DreamLoop", version="0.2.0") + app = FastAPI(title="DreamLoop", version=__version__) if auth_token is None: # The Host allowlist is DNS-rebinding protection for the # unauthenticated loopback mode. With token auth the token is the diff --git a/tests/test_packaging_release.py b/tests/test_packaging_release.py index 114e2fc..b9dab50 100644 --- a/tests/test_packaging_release.py +++ b/tests/test_packaging_release.py @@ -27,7 +27,7 @@ def test_project_version_is_consistent_across_package_and_web_app(): pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) app = create_app(ROOT) - assert pyproject["project"]["version"] == "0.2.0" + assert pyproject["project"]["version"] == "0.2.1" assert __version__ == pyproject["project"]["version"] assert app.version == pyproject["project"]["version"] diff --git a/uv.lock b/uv.lock index fcd5815..88c9f4e 100644 --- a/uv.lock +++ b/uv.lock @@ -367,7 +367,7 @@ wheels = [ [[package]] name = "dreamloop" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -480,7 +480,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [