Every link to r/SurfSense now points at https://www.reddit.com/r/SurfSenseAI/:
the website's navbar (desktop and mobile), the footer socials, the
homepage community strip and its label, the Organization sameAs in the
site's JSON-LD, and the backend's 'Join our Subreddit' incentive task.
Review of #2123. The route tests faked the license functions under the
routes, so the trial's folded-address refusal and resend skipping a
suspended license never ran. Keygen is now faked at its transport: a
tagged address is refused once the folded one holds a trial, a suspended
license is not mailed, and GET /license/file is covered. The contract
test no longer expects snake_case to miss, since Keygen underscores
every key; it proves a key with another word answers nothing. The email
bucket asserts the whole sequence, and the docs keep a narrowed gap for
the contract test's first live run.
The route and rate-limit suites were deleted with the fakes they stood on,
leaving the unauthenticated license routes without coverage. Resend and
trial are now driven over HTTP with Keygen and the mailer faked at the
router's imports and the real token bucket on its in-memory fallback, and
an opt-in contract test runs the Keygen client against a real Keygen to
prove the metadata filter spellings.
Refs #1964
_citation_payloads() split every marker payload on commas, URLs
included. The renderer only splits payloads that match its id-list
grammar and keeps a URL citation whole, so
[citation:https://news.example/7,114883,30573452,story.html] produced
chunk ids 114883 and 30573452: the wrong titles were attached, and a
piece >= 2**31 failed to bind to the Integer Chunk.id column and broke
the export.
Split a payload only when it fully matches the renderer's list grammar,
(?:doc-)?-?\d+(?:\s*,\s*(?:doc-)?-?\d+)*, and keep anything else as one
payload. When looking up titles, accept only plain ASCII integer ids in
the 32-bit range, so an oversized numeric id is skipped instead of
failing the query. doc- ids stay unresolved as before (they point at
SurfSense docs chunks, not workspace chunks).
Also apply ruff format to the test file, which the backend-lint hook
checks for every changed file.
Keygen v1.7.2 snake-cases every request key on the way in
(config/initializers/jsonapi.rb) and every metadata filter key
(License.search_metadata), and camelCases keys in responses, so
checkoutSessionId and checkout_session_id are one key. What returns an
empty list is a key with a different word. The keygen.py docstring, the
models.py comment and portal.md said filters were case-sensitive, and the
docstring cited a live contract test that does not exist yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
transformers is transitive (sentence-transformers, docling-ibm-models,
kokoro, unstructured-inference, chonkie, docling-core). None of them caps
it below 5: docling-ibm-models 3.13 declares >=4.42,<6 minus 5.0-5.3,
sentence-transformers 5.6 declares <6. The app does not import
transformers. transformers 5 needs huggingface-hub 1.x.
- transformers 4.57.6 -> 5.10.4 (lowest patched line)
- huggingface-hub 0.36.2 -> 1.20.1 (held to a release from the same
month as transformers 5.10.4), hf-xet 1.4.2 -> 1.6.0
Checked on Windows against the previous lock: the all-MiniLM-L6-v2
embedding, Kokoro synthesis and Docling's PDF conversion of
tests/fixtures/sample.pdf give identical output.
Advisories cleared:
- transformers 5.10.4: GHSA-29pf-2h5f-8g72, GHSA-69w3-r845-3855,
GHSA-fgcw-684q-jj6r, GHSA-x9r9-c232-4q39, GHSA-xrqw-3rrv-vx5w
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clears the 7 open Dependabot alerts on
surfsense_backend/scripts/whatsapp-bridge/package-lock.json. All four
packages are transitive, so only the lockfile changes; package.json is
untouched and lockfileVersion stays 3.
sharp 0.34.5 -> 0.35.4 (peer dep of @whiskeysockets/baileys; bundled
libvips 1.2.4 -> 1.3.3)
- GHSA-f88m-g3jw-g9cj (libvips CVE-2026-33327, -33328, -35590, -35591)
- GHSA-rgj7-g3m4-5g8c (libheif)
protobufjs 7.6.1 -> 7.6.6 (via baileys and libsignal)
- GHSA-f38q-mgvj-vph7 (CVE-2026-54269)
- GHSA-j3f2-48v5-ccww (CVE-2026-59877)
qs 6.15.2 -> 6.16.0 (via express and body-parser)
- GHSA-4mjr-xmp4-gh2g (CVE-2026-82417)
- GHSA-x5fp-wj9c-mxmx (CVE-2026-82562)
body-parser 2.2.2 -> 2.3.0 (via express 5.2.1)
- GHSA-v422-hmwv-36x6 (CVE-2026-12590)
Moved along with them: semver 7.8.5 (required by sharp 0.35),
side-channel 1.1.1 (required by qs 6.16), content-type 2.1.0 nested under
body-parser, @emnapi/runtime 1.11.3 (sharp-wasm32), and
@protobufjs/inquire dropped by protobufjs 7.6.6.
sharp 0.35 needs Node >= 20.9.0; the bridge image (node:20-alpine)
ships 20.20.x. sharp 0.35.5 (published 2026-09-27) was skipped as younger
than seven days, so versions were resolved with
npm update --before=2026-09-23.
Deferred: nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The <19.0.0 cap came in with dbd9c837e ("fix: docker version mismatch",
Dec 2025), which added datasets and pyarrow together and records no
reason for the cap. The Docker image has since been installed strictly
from a uv.lock export, so the drift that commit fought cannot recur.
Nothing in the app imports datasets or pyarrow; datasets was held at
4.0.0 only because every later release needs pyarrow>=21.
- pyarrow >=23.0.1 (lock 18.1.0 -> 23.0.1)
- datasets 4.0.0 -> 5.0.1 in the lock, which also fixes the path
traversal in folder-based builders (PYSEC-2026-3716, OSV only).
Advisories cleared (uv.lock and pyproject.toml alerts):
- pyarrow 23.0.1: GHSA-rgxp-2hwp-jwgg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The <0.51.0 cap and the sse-starlette <3.1.2 pin came in with the MCP
connector (305a981d1) and mirrored FastAPI's own starlette range at the
time (fastapi 0.128 required starlette<0.51.0). FastAPI supports
Starlette 1.x since 0.133.0, so the cap now only blocks security fixes.
- starlette >=1.3.1 (lock 0.50.0 -> 1.3.1), fastapi floor >=0.133.0
(lock unchanged at 0.135.2), sse-starlette pin lifted (lock unchanged;
only mcp's server side imports it, the app uses the MCP client).
- The app uses none of the APIs Starlette 1.0 removed (on_event,
on_startup/on_shutdown, @app.route/@app.middleware/@app.exception_handler
decorators, TemplateResponse(name, context), FileResponse(method=)).
Advisories cleared (uv.lock and pyproject.toml alerts):
- starlette 1.3.1: GHSA-82w8-qh3p-5jfq, GHSA-86qp-5c8j-p5mr,
GHSA-jp82-jpqv-5vv3, GHSA-wqp7-x3pw-xc5r, GHSA-x746-7m8f-x49c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trial check refused 20 hand-picked domains by exact match, so trial
spam walked around it with any other throwaway provider, any subdomain
of a listed one, or a punycode spelling.
Refuse the community disposable-email-domains list (~9.2k domains, CC0),
installed as its official PyPI package and pinned in uv.lock, plus the
three hand-picked domains it lacks. Match every parent domain short of
the TLD, as upstream requires, and look up the punycode form, since
EmailStr hands the route Unicode. Purchases and resends are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrM98FAv8YT3boAVJpf6PW
The chat export only recognised [citation:<id>]. Older assistant
messages also hold the other forms the web renderer accepts, from when
the model wrote markers itself: full-width brackets (【citation:12】),
zero-width spaces inside the brackets, and comma-separated ids
([citation:12, 34]). The first two were left in the exported text as
raw markers; for the third, "12, 34" never parsed as a chunk id, so
the message lost its source titles.
Match the same forms as the renderer and split comma lists into
separate ids. Also apply ruff format to the file, which the
backend-lint hook checks for every changed file.
When two documents in one folder share a title, the second gets a _2
suffix. The suffix was not checked, so if another document was already
titled "Notes_2", both were written to Notes_2.md: the ZIP held two
entries with that name and the manifest listed the path twice. Import
reads the last entry for both, so one document never arrived.
Keep counting until the path is free. Also apply ruff format to the
file, which the backend-lint hook checks for every changed file.
charge.refunded suspended every license the Stripe customer held, so a
partial refund, or a refund of one of two purchases, silently disabled a
license the customer still paid for. The handler now acts only on a full
refund, resolves the charge's payment intent to its checkout session and
that session to its one license, and suspends that license alone. A
refund that traces to no single license suspends nothing and logs.
Fixes#1962
correct_license_email.py could only find a license by its Stripe checkout
session, and a trial has none, so a mistyped trial address could not be
corrected. --trial-email finds the trial issued to exactly that address,
filtering on the typed email and its folded trial key so neither a
lookalike nor a paid license matches, and refuses when two answer.
Fixes#1965
A bad SMTP_* setting only raised when the mailer was first built, at send
time. /license/trial had already created the trial in Keygen by then, so
the caller got a 500 and their address was spent.
_require_mailer() now builds the mailer as well as checking SMTP_ENABLED,
so a configuration error answers 503 before any Keygen call on both POST
routes. That also stops /license/resend answering differently for
customers and non-customers on a misconfigured deployment. A 503 was
chosen over failing at startup so a mail setting cannot take down every
other route.
Fixes#1963
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 410 now carries the same sunset_url() that GET /health reads, so a
client refused a write can reach the sunset page without a second call.
Fixes#1938
Children of a block were rendered with the parent's numbered-list counter
and handed the result back to it, so a nested numbered list continued the
parent's numbering ("1. A / 2. A.1") and the next sibling skipped ahead
("4. B"). Children now count from zero and the parent's counter is kept.
The table-aware chunker only matched table lines ending in a newline, so a
table closing a document with no trailing newline lost its last row to a
separate prose chunk, splitting the table mid-row.
code-quality.yml ran every pre-commit hook minus a skip list in each job, so
newer hooks leaked in: check-docs ran four times per PR, the translation hooks
ran in every job, and Biome ran on every PR whether the web app changed or not.
The desktop backend had no security scan at all, the gates counted a cancelled
or timed-out job as passing, and hooks pinned tool versions a second time.
- code-quality.yml keeps only what belongs to no product, run by hook name on
every PR: file checks, check-docs and detect-secrets
- docker-tests.yml replaces backend-tests.yml and e2e-tests.yml for
surfsense_backend and surfsense_web: ruff, bandit, Biome, backend unit and
integration tests, and the e2e journey, each job run only when its files
change
- desktop-tests.yml filters per job instead of per workflow, adds bandit for
the desktop backend, and checks translations, failing on a stale en.json
that typecheck used to regenerate silently
- each workflow ends in a gate (Docker Gate, Desktop Gate, Quality Gate) that
always reports and fails on failed or cancelled jobs, so it can be made a
required check without blocking PRs that don't touch that product
- bandit is split into bandit-docker and bandit-desktop aliases, so each
product scans only its own code
- the Biome hook runs only when a commit touches surfsense_web, instead of
blocking every commit on the app's existing formatting errors
- Biome and formatjs run from each app's own install (pnpm exec, and a new
translations:verify script), so their versions live only in package.json
- hook versions updated: pre-commit-hooks v6.0.0, bandit 1.9.4, commitizen
v4.19.0, and ruff v0.15.8 to match surfsense_backend's own ruff
- surfsense_web's pnpm comes from its own package.json and the root
package.json is removed; every action is on its latest version
- CONTRIBUTING.md, docs/contracts/README.md, auth_mint.py, the translate
skill and localization.md name the new workflows and commands
Comments, docstrings and .env.example notes that cited moved or
deleted plan files now cite their new homes under docs/.
The three llama.cpp checksums in fetch-llamacpp.mjs and a URL-format
example in the backend config are marked `pragma: allowlist secret`.
Touching those files puts them in front of detect-secrets, and both
are false positives.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the engineering specs out of plans/ into a root docs/ folder.
architecture/ describes what is true in the code now, one doc per
feature. adr/ records decisions: 0003 is restored and 0004-0027 are
new. proposals/ holds designs not built yet: plugins and the CUDA
backend. contracts/ holds the frozen interfaces between trees, with
the fixtures their tests read. ROADMAP.md names the initiatives.
The shipped phase plans are deleted; git history keeps them. plans/
keeps business, launch and ops material only.
Also: CONTRIBUTING.md rewritten around this flow, a CODEOWNERS file,
a Docs section in AGENTS.md, scripts/check_docs.py (was
check_plan_links.py) as a pre-commit hook, the backend tests path
filter extended to docs/contracts, and the five tests that read the
contract fixtures pointed at their new path.
Co-authored-by: Cursor <cursoragent@cursor.com>
GitHub's Latest badge now points at the newest 2.x release instead of
legacy v0.0.40. The legacy 0.0.x desktop resolves updates through
/releases/latest and reads latest*.yml from whatever it names, so a
legacy-update-bridge job copies v0.0.40's manifests onto every release
with a `tag: v0.0.40` key. electron-updater honours that over the
release it resolved, so those clients still land on v0.0.40 and its
/sunset redirect, and the installers still come from v0.0.40's own
assets with nothing duplicated.
Artifact names drop ${version}, which makes
/releases/latest/download/<name> a permalink the README and the site
can hardcode. Safe for auto-update: a cached download is validated by
sha512 before it is installed, and every consumer labels assets by
suffix rather than by full name.
Also corrects docs and comments that asserted the pin was permanent,
including a stale bug report in the SEO brief -- the site's hero
already resolves by tag.
Co-authored-by: Cursor <cursoragent@cursor.com>