Repository navigation
Releases: opendefensecloud/knot
Release list
v0.8.1
Upgrading
- knot no longer trusts the
emSign ECC Root CA - G3root certificate. If your
OIDC provider, S3 endpoint or Postgres server presents a certificate that
chains to it, knot can no longer connect to it. Chrome is removing the same
root. (#43)
Security
- eMudhra CA key compromise (RUSTSEC-2026-0381, RUSTSEC-2026-0382). The
private keys of eight eMudhra intermediate CAs can be recovered from their
public certificates, so anyone can issue a certificate for any hostname that
knot accepted. This affected knot's connections to the OIDC provider, the S3
blob backend and Postgres. An attacker in the network path to the OIDC
provider could substitute their own signing keys when knot starts and then
sign in as any user, or capture the client secret and users' authorization
codes at login. knot's trusted roots are compiled into the binary and it
checks neither revocation nor Certificate Transparency, so eMudhra's
revocation and the browser blocks did not protect it, and there is no CA
bundle to update: upgrading is the only fix.rustls-webpki0.103.16
rejects chains through the eight intermediates andwebpki-roots1.0.10
drops the root. The Postgres change-notification connection was not
affected: it trusts onlysslrootcert. (#43) rustls0.23.46 fixes RUSTSEC-2026-0346, RUSTSEC-2026-0347,
RUSTSEC-2026-0348 and RUSTSEC-2026-0349. (#43)ammonia4.1.8 fixes RUSTSEC-2026-0373. knot was not affected: its
sanitizer does not allowstyleattributes. (#43)- RUSTSEC-2026-0367, RUSTSEC-2026-0368 and RUSTSEC-2026-0369 (
zip
extraction) are acknowledged indeny.toml. knot was not affected: it reads
import archives in memory and never extracts them to disk. The fix needs
zip 9. (#43)
Container image
ghcr.io/opendefensecloud/knot:0.8.1 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.8.1 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- fix(deps): distrust the compromised eMudhra CAs (0.8.1) by @christianhuening in #43
Full Changelog: v0.8.0...v0.8.1
v0.8.0
Upgrading
- Two database migrations run on upgrade (the Helm chart's migrate hook applies
them). - Pages deleted in earlier versions move to the archive with a 30-day
retention starting at the upgrade, and are then permanently deleted unless
restored. To change the period, setKNOT_ARCHIVE_RETENTION_DAYS(Helm
archive.retentionDays) before upgrading. - Subpages of previously deleted pages are moved to the top level, where the
sidebar already showed them. - Contributor lists count edits from this version on only.
Added
- Page byline showing who created the page and who has edited it, with their
last edit.GET /api/docs/{id}/contributors. (#35) - Resizable table columns. Widths are stored as proportions, so tables fill the
page width on any screen. Widths are not kept through Markdown export/import.
(#38) - Document archive. Deleting a page moves it and its subpages to the archive,
where editors and owners can preview and restore it, and owners can delete
it permanently. Archived pages are purged afterKNOT_ARCHIVE_RETENTION_DAYS
(default 30).GET /api/workspace/archive,POST /api/docs/{id}/purge.
(#39)
Changed
- Archived pages are read-only. Writes return
409 doc.archived, open editors
switch to read-only, and share links stop working. Archived pages are left
out of search, My Tasks and workspace export. (#39) - Tables with columns resized in earlier versions now fill the page width
instead of keeping fixed pixel widths. (#38) - A link now ends where its text ends. Retyping a link's last character, or
typing over all of its text, gives plain text. (#40)
Fixed
- Edits made while offline never reached the server and were lost on reload.
Boards had the same problem. (#35) - Revoking access did not disconnect open editors, and their edits kept being
saved. (#35) - Live edits were stored without an author, and batched edits were credited to
the first author in the batch. (#35) - An open editor wrote an autolink edit in response to another user's typing.
(#35) - Editors could not delete pages; only owners could. (#39)
- Deleting a page left its subpages at the top level of the sidebar and in
search. (#39) - The sidebar row menu could not be used with the keyboard. (#39)
- A page whose access changed while it was open showed "You no longer have
access" even when you could still read it. (#39) - Column resizing did not work on existing pages. (#38)
- Long URLs in table cells overflowed into the next column. (#38)
- Text typed after a pasted link, or at the edge of any link, became part of
the link. (#40) - Pasting a URL over selected text left the new link selected, so the next
keystroke replaced it. (#40) - The toolbar Link button opened empty with the cursor at a link's edge, and
applying the empty value removed the link. With nothing selected, Apply now
inserts the URL as link text. (#40) - Typing or pasting
https://…opened the date picker, which took the
keyboard focus. (#40)
Security
ammonia4.1.7 fixes RUSTSEC-2021-0157. knot was not affected: its
sanitizer does not allowstyleattributes. (#38)- RUSTSEC-2024-0449 (
unsafe-libyaml, unmaintained) is acknowledged in
deny.toml. It has no known vulnerability and only parses the operator's
own config file. (#38)
Container image
ghcr.io/opendefensecloud/knot:0.8.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.8.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- feat(docs): page byline (creator + contributors); fix(collab): offline edits never reached the server by @christianhuening in #35
- feat(editor): resizable table columns with proportional widths (#36) by @christianhuening in #38
- feat(docs): archive deleted pages with 30-day retention (#37) by @christianhuening in #39
- fix(editor): end links where their text ends by @christianhuening in #40
- docs: changelog for 0.8.0 by @christianhuening in #41
Full Changelog: v0.7.0...v0.8.0
v0.7.0
Added
- Resizable sidebar: drag its right edge (200–480px), or focus it and use ←/→
(Shift for larger steps) and Home/End; double-click resets it to 260px. The
width is saved per browser and synced across tabs. (#33) - Histograms
knot_collab_initial_sync_secondsandknot_room_hydrate_seconds
for document-open time. (#32)
Changed
- Faster document open: hashed assets are brotli-compressed and cached as
immutable (index.htmlisno-cache), the editor loads in parallel with the
document, and images and attachments revalidate viaETag. (#32) - With comments open, the document is inset only while the content column
(window minus sidebar) is at least 1020px wide, replacing the fixed 1280px
breakpoint. (#33)
Fixed
- Latency metrics were exported as summaries, leaving the Grafana latency
panels, SLO queries and latency alert without data; they are now histograms
with_bucketseries. (#32) - Deep links such as
/docs/<id>returned 404 (while still rendering); they
now return 200, and a missing/assets/*file returns a plain 404. (#32)
Container image
ghcr.io/opendefensecloud/knot:0.7.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.7.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- perf: faster doc open — compressed/immutable assets, editor preload, blob ETags, doc-load metrics by @christianhuening in #32
- feat(web): make the sidebar resizable by @christianhuening in #33
- docs: changelog for 0.7.0 by @christianhuening in #34
Full Changelog: v0.6.0...v0.7.0
v0.6.0
See CHANGELOG.md.
Container image
ghcr.io/opendefensecloud/knot:0.6.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.6.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- fix: subpage create conflict by @AlexHardAtWork in #27
- chore(nix): update rust-overlay so Rust 1.97.1 resolves by @AlexHardAtWork in #25
- fix Bug #29 by @christianhuening in #30
- implement additional skins by @christianhuening in #31
New Contributors
- @AlexHardAtWork made their first contribution in #27
Full Changelog: v0.5.0...v0.6.0
v0.5.0
The editor moves from Tiptap 2 to Tiptap 3. The visible result should be nothing
at all — almost all of the work went into keeping the stored document schema
identical to what every existing page was written against — but four things that
were quietly not working are fixed on the way, and a browser that navigates away
mid-request can no longer strand an open transaction on the server.
Changed
- The editor runs on Tiptap 3 (2.27.2 → 3.31.3, and the whole
@tiptap/*
set with it). The 2.x line is not dead — it still has av2-latesttag and
picked up a security backport as recently as 2.27.3 — but it is maintenance
only, so every fix and every new extension now lands on 3.x alone. Two things
made this more than a version bump. The Yjs binding changed hands:
y-prosemirroris replaced by@tiptap/y-tiptap, Tiptap's own fork, and both
create their plugin key by the same name — so importingySyncPluginKeyfrom
the wrong one leaves ProseMirror holdingy-sync$in one place andy-sync$1
in the other,getState()silently returnsundefined, and every comment
anchor resolves to null with nothing thrown. AndCollaborationCursoris gone
in favour ofCollaborationCaret, which sanitises a colour it cannot parse to
transparentrather than rendering it anyway. - The stored schema is deliberately unchanged. Everything StarterKit 3 adds
that would have written to a document —trailingNode,listKeymap, and its
now-bundled Link and Underline — is disabled, and the snake_case node names
knot's Rust serialiser expects are preserved. A 0.4.0 document opens on this
release producing an identicalY.XmlFragmentand emitting no Y.Doc updates
at all. The only schema difference any stored page sees is the linktitle
attribute, which is a fix — see below. - The exact
prosemirror-*pins are gone. They existed to deduplicate the
ProseMirror core that@tiptap/pmandy-prosemirrorboth pulled in, and
y-prosemirrorleft with the migration. They had also started doing harm:
@tiptap/pm3.31.3 asks forprosemirror-model^1.25.11 and
prosemirror-view^1.42.3, and the overrides held the tree at 1.25.7 and
1.41.8 — below both, silently, because pnpmoverridesbypass range checks.
Added
- Renovate keeps dependencies current (
.github/renovate.json). Nothing was
watching them between manual refreshes. Patch and minor updates merge
themselves once nextest, vitest, Playwright, clippy and cargo-deny are green;
majors never open a PR on their own, so a migration like this one stays a
deliberate choice rather than eight red PRs landing at once. Weekly
lockFileMaintenancerefreshes the lockfiles, which is the only route by
which a patched transitive dependency ever arrives. The Rust, pnpm and Node
versions that each live in three files are grouped so they move atomically.
CONTRIBUTING.md § Dependencies is the reference. - A test baseline for the migration, landed on Tiptap 2 first, so it is a
real before/after rather than a story told afterwards.
web/src/test/boundEditor.tsmounts a real editor over a realY.Docin
jsdom in about 40ms — the assumption that this needed Playwright is what left
the gaps. Comment anchors now resolve through the live ySync binding instead
of a hand-built Map;schema.test.tspins the exact registered-extension list
and checks attribute parity in both directions againsttools/schema.json;
ydoc.test.tsasserts the editor writes nothing but the user's own edits.
Two gaps needed the whole stack:comment-anchors.spec.ts, because the
existing comment spec posted every comment straight to the API with
position_y: nulland never exercised anchoring at all, and
editor-markdown-roundtrip.spec.ts, because only paragraph, image and
excalidraw_boardhad ever travelled editor → Y.Doc →to_markdown. pnpm dedupe --checkgates CI. Not belt-and-braces: while removing the
pins above, the ProseMirror core was genuinely split for one run —
@tiptap/pmonprosemirror-view1.41.8 while@tiptap/y-tiptapwas on
1.42.3 — and tsc, eslint and all 153 unit tests passed anyway. TypeScript is
structural, so the twoNodeclasses match and nothing else in CI looks at
resolution.
Fixed
- A browser navigating away mid-request could strand a transaction and stall
the server.sqlx'spool.begin()is not cancellation-safe: drop the
future after BEGIN has reached Postgres but beforebegin()returns, and sqlx
never receives aTransactionto roll back and keeps no record that one is
open. The connection goes back to the pool looking clean, and every later
query handed it silently joins the orphan. One backend was caught holding a
dozen unrelated documents' work inside a single transaction open for minutes,
with AccessShare ondoc_updates,board_updatesandcommentsplus
RowExclusive onsessions. Nothing ever commits it: it pins a transaction id
against vacuum and stalls any TRUNCATE or DDL on those tables until the
process exits. axum drops a handler future exactly this way whenever a client
disconnects, which is ordinary browser behaviour.knot_storage::beginnow
runs the BEGIN on a detached task, so the caller can be cancelled but the
transaction is always constructed and always dropped — and sqlx's ownDrop
rolls it back. Connections also carryapplication_name=knot-serverand a 30s
idle_in_transaction_session_timeoutas a backstop. ⌘⇧8and⌘⇧7did nothing. knot renames ProseMirror's nodes to
snake_case, but renaming a node does not rewrite the options its own commands
read: BulletList and OrderedList still resolveditemTypeNameto the
camelCaselistItem, sotoggleBulletList()raised "There is no node type
named 'listItem'" and the keystroke logged instead of making a list. The
toolbar buttons pass both names explicitly, which is why only the shortcuts
were affected.- Link titles were dropped from storage.
tools/schema.jsondeclares
titleon the link mark and bothfrom_markdownandto_markdownhonour it,
but Tiptap's Link has no such attribute and ProseMirror discards what its
schema does not declare.[text](url "title")survived import and then lost
its title the first time anyone edited the page — in the CRDT, for every
reader and for the next export. Nothing backfills: titles already lost this
way stay lost. - Code blocks, Mermaid diagrams and Excalidraw boards ignored wide mode.
0.4.0's notes said all three break out into the room; only tables actually
did. A React node view's outer element is Tiptap's owndiv.react-renderer
and the wrapper carrying our attributes renders inside it, so
.ProseMirror > preand.ProseMirror > [data-testid=…]selected nothing —
invisibly, because the generic.ProseMirror > *rule still applied and held
all three at the 712px prose measure. Retargeted at thenode-<type>class
Tiptap stamps on the wrapper, anddoc-width.spec.tsnow measures rendered
widths rather than trusting a selector. - Every collaborative session logged "A user uses an unsupported color
format" once per peer.colorFor()emittedhsl(); the caret accepts only
#rrggbb. Noise under Tiptap 2, which rendered the caret anyway — but the v3
caret extension replaces a colour it cannot parse withtransparent, which
would have made every remote caret invisible. Same palette, converted rather
than re-picked, so nobody's avatar changes colour.
Security
prosemirror-view1.42.3 closes an XSS in clipboard handling: attribute
validators were not run on attributes arriving via slice context. No advisory
was ever filed, so no scanner would have surfaced it — the old pins were
holding knot at 1.41.8. knot was not reachable through it, because Tiptap only
setsspec.validatewhen an extension declares a validator and knot declares
none, but being on the patched release closes the exposure the moment that
stops being true.- GHSA-cp6q-959q-f8rh (
mergeAttributes()prototype pollution) is closed by
@tiptap/core3.31.3. knot was never reachable — ProseMirror builds
node.attrsinto a null-prototype object over schema-declared names only —
and the advisory range no longer flags the shipped version.
Operators — read before upgrading
- No migrations, and rollback to 0.4.0 is clean. Nothing in this release
changes the database schema. - Tell people to reload. The JS bundle is content-hashed and code-split, so
a tab left open across the upgrade can request a chunk that no longer exists
and getindex.htmlback instead. A refresh fixes it. - A stale 0.4.0 tab strips link titles. 0.4.0's schema does not declare the
linktitleattribute fixed above, so if an old tab edits a link run it
rewrites it without the title, for every peer. The same applies if you roll
back. It only affects titles, which are new in this release. - Comments anchored to the very first character of a document lose their
inline highlight.@tiptap/y-tiptaprejects an item-based relative position
resolving to absolute position ≤ 1, which is how 0.4.0 stored an anchor that
started at the first character. Nothing is deleted — the thread, its body and
its quoted text all still appear in the sidebar — and it self-heals as soon as
any content is inserted above the anchor. Comments created on this release are
unaffected.
Container image
ghcr.io/opendefensecloud/knot:0.5.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.5.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: [chart README](https://github.com/opendefe...
v0.4.0
The document column can be widened to use the window, and several things that
were quietly broken at the edges of any width are fixed.
Added
- Fixed / wide document width. A toggle in the doc header,
⌘⇧F, a command
palette action, and Settings → Appearance. One global per-user preference
(knot.docWidth), stamped before first paint so there is no narrow-to-wide
flash. Not role-gated — width is a reading preference, so viewers get it too;
hidden below 1024px, where it would buy nothing. - Wide mode widens the container, not the measure. Paragraphs hold the same
712px column and do not reflow a line, while tables, code blocks, Mermaid and
Excalidraw cards break out into the room. Content box at a 1920px window goes
712px → 1472px, capped at a 1600px shell so an ultrawide does not sprawl; prose
stays at 95 characters per line in both modes. The practical win is tables: a
five-column table goes from 125px cells — six wrapped lines each — to 265px. - The comment sidebar reserves space instead of covering the page at ≥1280px.
It previously sat on top of a constant 352px of every line at every window
size, so a wider monitor never helped. - Favicon, app icons and a web manifest. An SVG mark with PNG fallbacks
(32px, apple-touch, 192/512, maskable), regenerable viatools/gen-favicon.mjs.
Public/p/{token}pages link it too — a share link is often someone's first
sight of knot.
Fixed
- The mention and datetime popups could open off-screen. Both are
position: fixedchildren of<body>placed at the caret'sleftwith no
right-edge clamp. They only ever fit because the fixed column kept the caret
~450px from the window edge; in wide mode at 2560px the datetime picker's Apply
button landed outside the viewport, and beingfixed, could not be scrolled to. - The floating "Add comment" button could give the app a horizontal scrollbar.
It was clamped only on the low side, so a selection near the right edge pushed
it past its host;<main>isoverflow-y: auto, which makes itsoverflow-x
compute toauto, turning that into an app-wide scrollbar. Its "don't cover the
toolbar" guard also measured against the ProseMirror top rather than the sticky
toolbar, so it never actually held — the editor scrolls while the toolbar stays
pinned. - Tables with dragged column widths overflowed for readers. Tiptap only mounts
its table wrapper when the editor is editable, so view mode — the default — had
nothing to scroll a pinned table inside.colwidthis stored in the CRDT, so one
collaborator's column drag blew out the page for everyone reading it. - The document header overflowed on phones. At 375px the page scrolled 73px
horizontally and the last two controls, Save as template and Comments, sat
off-screen and unreachable: the action row isshrink-0with no wrapping, so it
forced its intrinsic 412px into a 327px content box. Both rows now wrap. This
also repairs a quieter tablet case — at 768px the title had been squeezed to
36px beside the action row. - Comment highlights hidden behind the sidebar could not be scrolled into view.
scrollIntoView({ block: "center" })only corrects the vertical axis; insetting
the document means highlights are never underneath the rail to begin with.
Changed
- The e2e suite could wedge the server permanently. Every spec reset with a
bareTRUNCATE ... CASCADE, which needs ACCESS EXCLUSIVE on tables the CRDT room
actors keep busy for as long as a document is open. Postgres queues lock requests
FIFO, so the pending TRUNCATE then blocked every query behind it — one reset
could park the whole server for the rest of the run, recoverable only with
pg_terminate_backendand a restart.SET lock_timeoutplus a retry fixes it:
on timeout the statement aborts and stops queueing, so the server is released
immediately. A full local run is now 50 passed / 1 failed in 2.6 minutes.
(A row-by-row DELETE also avoids the queue and was measured first — it is worse,
because without the exclusive lock it races the writers it no longer excludes and
leaves connections in aborted transactions.) - 31 copies of the e2e
reset()helper are now one. They had drifted: several
omittedboards,share_tokensanddoc_tasks, so those tables leaked between
specs. editor-toolbarandhistoryspecs select text withControlOrMeta+a. On macOS
Chromium bindsControl+Ato "move to start of line", so the selection collapsed
and Bold applied to nothing — a failure that could only ever reproduce off CI.
Container image
ghcr.io/opendefensecloud/knot:0.4.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.4.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- fix(e2e): stop the suite wedging itself, and fix select-all on macOS by @christianhuening in #12
- feat(editor): toggleable fixed / wide document width by @christianhuening in #11
- feat(web): give knot a favicon, app icons and a web manifest by @christianhuening in #13
Full Changelog: v0.3.0...v0.4.0
v0.3.0
Markdown documents can be brought into knot — as a file, or by pasting the source.
Added
- Import a Markdown file as a page. "Import Markdown…" sits next to Export on
the doc page, for editors and owners. 1 MB limit. Importing into a page that
already has content asks first; the previous version stays in History. - Markdown-aware paste. Pasting Markdown source now produces real headings,
lists, tables and task items instead of literal## Headingtext. Left alone
when the clipboard carries rich text, inside a code block, and on ⌘⇧V. POST /api/docs/{id}/markdownaccepts?mode=replace, which clears the page
before applying. The default staysappend, so existing callers — including
create-from-template — are unchanged.
Fixed
- Importing over a page that had content duplicated it instead of replacing.
The endpoint only ever merged into the live document; its one caller always
targeted an empty page, so the path was never exercised. - Checklists built server-side rendered as plain bullets.
from_markdown
storescheckedas the string"true"/"false", but the editor matched only
the boolean form. Affected imported files and pages created from a template.
The Markdown round-trip and the task index were correct throughout; only the
rendering was wrong. - A full-document replace is attributed to the user who made it, rather than
landing indoc_updateswith a null author. h20.4.15 → 0.4.19 (RUSTSEC-2026-0258,
unbounded empty DATA frames) andchacha200.10.1 → 0.10.2 (yanked). Both
transitive, via hyper and rand.
Changed
pnpm lintruns in CI, and the backlog it had accumulated unseen is cleared.
Two of those were real defects:onClickandonPickTemplateare typed to
return void, so their inlineasynchandlers left promise rejections
unhandled — a failed request surfaced as an unhandled rejection instead of the
intended error toast.excalidraw.specresets per test instead of per file. Its two "flaky" specs
were deterministic failures: every test bootstraps through/setup, and
POST /auth/setupreturns 410 once a user exists, so tests 2 and 3 sat on the
setup page until they timed out. The playwright job dropped from ~10m to ~5m.
Container image
ghcr.io/opendefensecloud/knot:0.3.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.3.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- fix(e2e): reset per test in excalidraw.spec by @christianhuening in #7
- feat: import a Markdown file as a page by @christianhuening in #9
Full Changelog: v0.2.1...v0.3.0
v0.2.1
Two bugs that CI could not see, and the two tests that now see them.
The Helm chart could never be installed — charts 0.1.0 and 0.2.0 both
fail at the very first step of helm install. And restoring a document
snapshot corrupted the document for every connected client, appending the
restored text to the existing content instead of replacing it.
Fixed
-
Restoring a snapshot merged instead of replacing.
ReplaceWithMarkdown
clears the document fragment and applies the restored content in a single
transaction, but it broadcast and persisted the caller'supdate_bytes—
which encodes only the insertion. Peers therefore received the insert without
the delete and kept what they already had:expected: "First version of the doc." actual: "Completely different content.First version of the doc"Because the persisted update was also missing the deletion, this was a
data-integrity bug and not merely a display glitch: replaying the update log
reproduced the merged content. The room now captures what the transaction
actually produced viaobserve_update_v1— the same pattern
PatchTaskCheckedalready used — and persists and fans that out.The existing
replace_with_markdown_swaps_contenttest could not catch this:
it asserted on the room's own document, which was always correct. The new
replace_broadcast_replaces_peer_contentasserts on the frame a peer
receives, applied to a peer holding the pre-restore state.Affects every release with document history:
0.1.0and0.2.0. -
The chart could never be installed. The
pre-install/pre-upgrade
migrate Job injectedKNOT_DATABASE_URLbut notKNOT_SESSION_KEY. The binary
loads and validates its entire config before dispatching the subcommand, and
validation rejects an empty session key — so the hook exited 2 with
KNOT_SESSION_KEY is requiredand everyhelm installandhelm upgrade
failed before reaching the Deployment:Error: INSTALLATION FAILED: failed pre-install: resource Job/knot-migrate not ready. status: Failed, message: Job Failed. failed: 1/1 config: invalid: KNOT_SESSION_KEY is required (set it in every environment)migratenever reads the session key; it only has to survive validation. The
Job now receives it from the same Secret the Deployment uses, honouring
session.existingSecretName/existingSecretKey.The bug survived two releases because
ct installis disabled in chart CI, so
nothing had ever deployed the chart —helm lintcannot see a hook that fails
at runtime.
Added
-
Helm upgrade test (
.github/workflows/helm-upgrade.yaml). Installs the
previous published release — chart pulled from ghcr and the real image, not
the working tree — seeds a document through that release's own API, runs
helm upgradeto the working tree, and then asserts:/api/versionchanged — the binary actually swapped- the session cookie minted by the old version still authorises — the
session format and signing key survived - the seeded document still reads back — schema and migrations survived
Runs on chart, migration and
Dockerfilechanges, weekly againstmain, and
on demand. This is what caught the migrate Job bug. -
replace_broadcast_replaces_peer_content, a room-level test asserting on the
bytes a peer actually receives from a restore rather than on the room's own
document. Reverting the fix turns it red with the exact production symptom
(<paragraph>Replaced Content</paragraph><paragraph>Hello World</paragraph>),
while the pre-existing test stays green.
Changed
history.specno longer restores an arbitrary snapshot. It selected
snapButtons.last()— the oldest — which withKNOT_SNAPSHOT_EVERY_N=1is
the complete V1 only when every keystroke lands in one persisted batch: true
on a fast machine, false on a loaded runner, so the spec restored a prefix and
asserted on the full string. Its preview check only required"First version",
which any prefix satisfies, so the failure surfaced later and read as a restore
bug. It now pins the newest snapshot that contains all of V1 and asserts the
full string in both places.
Operators
- Upgrading from chart
0.2.0. If you worked around the broken hook with
--set migrations.enabled=false, drop that override —0.2.1runs migrations
itself again. Nothing else about your values file changes. - Installing fresh.
helm installnow works with defaults; no manual
/knot-server migratestep is needed. - The stray
0.3.4chart package has been deleted from ghcr, so
helm install oci://ghcr.io/opendefensecloud/charts/knotwithout--version
resolves to the newest real release instead of the mis-versionedv0.1.0
chart. The--versionadvice in the 0.2.0 notes below is no longer required. - The Prometheus
routelabel change noted under 0.2.0 still applies if you are
coming from0.1.0.
Container image
ghcr.io/opendefensecloud/knot:0.2.1 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.2.1 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- fix(helm): give the migrate Job the session key; add an upgrade test by @christianhuening in #6
Full Changelog: v0.2.0...v0.2.1
v0.2.0
Container image
ghcr.io/opendefensecloud/knot:0.2.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.2.0 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- docs: add Claude Code skill for scripting the knot REST API by @ducke in #4
- chore(deps): full dependency refresh for v0.2.0 by @christianhuening in #5
New Contributors
Full Changelog: v0.1.0...v0.2.0
v0.1.0
Container image
ghcr.io/opendefensecloud/knot:0.1.0 — multi-arch (amd64/arm64), cosign-signed, with SBOM + SLSA provenance.
Helm chart
helm install knot oci://ghcr.io/opendefensecloud/charts/knot \
--version 0.3.4 \
-n knot --create-namespace \
--set database.url='postgres://…' \
--set session.key="$(openssl rand -base64 32)" \
--set baseUrl=https://knot.example.comFull values: chart README.
What's Changed
- Security & AuthN fixes and improvements by @christianhuening in #2
- Quality of Life improvements by @christianhuening in #3
New Contributors
- @christianhuening made their first contribution in #2
Full Changelog: https://github.com/opendefensecloud/knot/commits/v0.1.0