485c8fa538
328 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 485c8fa538 |
#303 follow-up: the fonts volume must mount node-owned
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m49s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 1m21s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m35s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
CI / Import/export fidelity gate (push) Successful in 57s
CI / Build container images (pull_request) Successful in 2m52s
CI / Lint, typecheck, test (pull_request) Successful in 6m28s
CI / Import/export fidelity gate (pull_request) Successful in 57s
Found on the real deploy, not in any test: `/data/fonts` in the running api container was `root:root` and the non-root `node` user could not write to it. Every upload would have failed with EACCES at runtime while the api reported ready. The api Dockerfile already explains the mechanism for uploads and plugins — Docker copies an image directory's ownership into a fresh named volume on first mount — and pre-creates them chowned. #303 added `CUSTOM_FONTS_DIR` to the ENV but not to that mkdir/chown line. Adds a CI fence so it cannot recur: every `/data/…` path the api image defaults to must also appear in the mkdir AND the chown. Verified against the actual defect — removing `/data/fonts` from the chown makes it fail. |
|||
| b96997501a |
#303: operator-uploaded fonts — storage, API, PDF embedding, backup
All checks were successful
CI / Build container images (pull_request) Successful in 3m53s
CI / Auth e2e pack (pull_request) Successful in 8m42s
CI / Auth e2e pack (push) Successful in 8m41s
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 18s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Deploy to Test (push) Successful in 16s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m41s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 52s
An operator holding a font licence could only use it by baking the file into a custom image, which tied every change to a rebuild and left the file out of the backup. ADR 0016 said there is no runtime font management. It also listed this exact case under "Alternatives considered" — *may become a Site-Admin- level feature later*. The amendment takes that option and answers the two objections it raised: licensing risk (Site Admins only, licence recorded with the family) and file-format attack surface (magic-byte check and a size cap, never a parse). - `CUSTOM_FONTS_DIR` (default `./data/fonts`) — a sibling of uploads and plugins, NOT inside the image-baked `FONTS_DIR`, where a deploy would overwrite it and no backup would ever see it. - One list of data directories (`apps/backup/src/data-dirs.ts`) now feeds both the nightly archive and the restore, so they cannot drift. #306 and #307 add one line each instead of a second mechanism. - Both Dockerfiles bake the path. The backup image sets its volume paths itself ("self-sufficient without compose env" — #71's lesson) and reads no *_DIR from compose; without the ENV entry the archive would have skipped the directory silently. - The PDF path already read WOFF2 from disk at request time, so it only had to pick the other base directory for a custom family. - `fontStack`/`fontEntry` take the instance's uploaded families as an argument — they are runtime data. The catalog is searched first, and a colliding family name is rejected at upload, so a custom font can never shadow a catalog one. - Deletion is never blocked by usage: an unknown family already falls back to the system stack, so affected ponds degrade instead of breaking. The count of affected ponds travels into the audit entry. - Audit catalogue v1.6 (`font.uploaded`, `font.deleted`). Verified: api full suite against a fresh database, 102 files / 571 tests. The upload suite writes into a real temp directory and reads the bytes back off disk, so the storage layer is exercised rather than mocked. |
|||
| 5164801676 |
#301: reset the login rate limit before the VS-NfD packs
All checks were successful
CD / Promote to Int (push) Successful in 12s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 58s
CI / Lint, typecheck, test (push) Successful in 6m32s
CI / Auth e2e pack (push) Successful in 8m30s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 8m42s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CD / Build and push images (push) Successful in 18s
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Deploy to Test (push) Successful in 14s
CI 665: the reflow guard itself passed; the run died two packs later on `fixture login for fixture-admin failed: 429`. The a11y pack costs one more login since this branch added the reflow test, and that was enough to exhaust the budget before the VS-NfD packs. Same trap the workflow already documents for the content and collab packs — it just needed one more reset, in the place the extra login pushed it over. |
|||
| 69882ecbea |
#301: the token tables need the same scroll wrapper
The sorted report finally named it: `table.api-tokens__table` at 833px wide, with its `.visually-hidden` heading reaching right=737 — exactly the document's scrollWidth. Same mechanism as the sessions table, a second table I had not wrapped. Locally the API-tokens table was empty and therefore narrow, which is why this only ever appeared in CI. With a token present it reproduces: without the wrapper 345px of page overflow, with it none. The feed-token table gets the same treatment — it is built the same way and would fail as soon as someone holds a feed token with a long name. The "[in fitting scroller]" marker in the report is misleading for these: `main.main` is a scroller, but it is `position: static`, so it never clipped the absolutely positioned heading. Only a positioned ancestor does — which is what `.table-scroll` now is. Verified locally against a real stack, with a wide token table present: reflow guard green, whole a11y pack green in both colour schemes. |
|||
| 2422f3a28f |
#301: sort the reflow report so the culprit cannot be buried
CI still reports 737 while the local stack is now clean, and the box list was capped at 15 entries — all of them nav links clipped by their own scroller. Whatever pushes the page in CI sits past that cap. The list is now sorted by reach, marks each entry as either clipped by a fitting scroller or actually pushing the page, and shows 40. |
|||
| b65339ae13 |
#301: the overflow was an escaping visually-hidden heading
Found by standing up the local stack instead of guessing through CI. The DOM tree under `.app-body` shows it in one line: span.visually-hidden rect=[342,343] pos=absolute Its right edge is 343, and `.app-body` reports scrollWidth 343 against a 320 client. The table's actions column carries a `.visually-hidden` heading, which is `position: absolute`. `.table-scroll` was `position: static`, so it was NOT that span's containing block — the span escaped the scroller's clipping, kept its static position out at the table's right edge, and pushed the page. `position: relative` on the wrapper makes it the containing block, and the span is clipped like the rest of the table. This is one cause behind both numbers: 23px locally, matching the original report, and 417px in CI, where different font metrics make the table wider and carry the span further out. Chasing them as separate problems is what cost three CI rounds. Verified locally against a real stack: the reflow guard passes and the whole a11y pack is green, 11 tests in both colour schemes. |
|||
| 194f144797 |
#301: dump raw box metrics from the reflow guard
Two rounds now reported no element past the viewport edge while the document still claimed 417px of overflow — a combination that rules out every hypothesis I had, including my own filter. So stop inferring. The guard now prints the html/body metrics, every element whose own content is wider than its box (with its overflow-x, so the intentional scrollers are distinguishable), and every box reaching past the edge with no filtering at all. Diagnostics ride in the assertion message, not the compared value, so they show up even when they match. |
|||
| 9fce824a8e |
#301: make the reflow guard report the ancestor chain
The previous run came back with an empty offender list and an unchanged 417px overflow: the filter treated everything under a scroll container as innocent, including the container that was itself too wide. A scroller only absolves its children when the scroller fits. It now reports the chain from body down to the widest offender with each box's width, so the first element wider than the viewport is visible instead of inferred. |
|||
| 18c2ed0bfe |
#301: the real culprit was the jump nav, not the wide content
The first attempt fixed plausible suspects. CI measured the actual page and named something else: six `.settings-nav__link` buttons, 417px of page-level overflow at 320px. `.settings-nav` already had `overflow-x: auto`, but as a flex child it also had the default `min-width: auto` — the min-content width of the whole jump strip. That forced the column wider than the viewport, so its own overflow rule never had anything to scroll. `min-width: 0` is exactly the case CLAUDE.md warns about under Reflow. The guard now ignores elements that sit inside a scroll container. Such content is *meant* to be wider than the viewport — reporting it buried the one finding that mattered under twelve lines of noise, and the cap truncated the list before it could show anything else. The table wrapper and the wrapping settings rows from the first commit stay. Neither was the cause here, but a table cannot shrink below its min-content width and those rows cannot wrap on their own, so both are hardening that holds regardless of content. |
|||
| f938ee9880 |
#301: stop /settings scrolling horizontally at 320px
WCAG 2.1 SC 1.4.10 asks for no two-dimensional scrolling down to 320px, which is also what 400% zoom on a 1280px screen produces. The layout skeleton was already hardened for this in #165; the overflow came from content inside the sections. - The sessions table cannot shrink below its min-content width — four columns, one of them the full user-agent string. It now scrolls inside its own container rather than pushing the page. The container is focusable with a role and a name, because a scroll area that only a mouse can reach trades one barrier for another. - `.settings-checkbox` rows may wrap. The accent swatches have a fixed size and cannot shrink, so an unwrappable row set a floor for the whole page width. Adds a reflow guard to the a11y pack. axe does not cover 1.4.10 — the criterion is not derivable from the DOM — so this is a separate check, and it names the overflowing elements when it trips instead of only reporting that something overflows. |
|||
| f9149eba13 |
#302: the vault import test reaches its page through the sidebar
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 35s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Deploy to Test (push) Successful in 14s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m49s
CI / Import/export fidelity gate (push) Successful in 1m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
The Obsidian fixture vault contains a note called "Startseite", and the pond now creates one too — the seeded fixtures use locale `de`. Two consequences, and the second is the one that mattered: - the unscoped title locator matched two sidebar entries; - `/p/<pond>/startseite` no longer belongs to the imported note. The pond's own start page took that slug, so the import landed on a suffixed one and the test was about to assert against the wrong page. Both are fixed by scoping to the mount page and navigating through the sidebar instead of guessing a slug. The test stays meaningful: it then clicks a wikilink inside the page content, which the empty auto-created start page would not have. CI caught this; the local run passed it. Worth remembering that a title-based locator can go green by luck. |
|||
| 30fd1ff53b |
#302: the permission matrix counts the start page
Every pond created through the api now carries one, and the matrix pond is created that way. The start page is an ordinary page with no grant of its own, so it follows the pond-wide permissions: the three member subjects each see one more, the label-restricted editor too, and the outsider — who reaches only the explicitly public page — still sees one. The 429 in the same run was the login rate limit, reached through the retries of this failure rather than on its own. |
|||
| 45f1925917 |
#302: configurable pond start page, created with every new pond
Opening a pond landed on whatever sorted first in the sidebar — stable, but a rule nobody could see, and one whose target moved as soon as someone added a page ahead of it. New ponds landed on the empty-pond hint instead of anything useful. - `startPageId` joins the pond settings. No migration: `Pond.settings` is already jsonb. It stores an id, not a slug, so renaming or moving the page keeps it working. - `PondHomePage` prefers it, but only when the page is in this user's page list. That list already holds just what they may see, so a start page hidden by a page-scoped grant — or trashed — falls back silently instead of landing them on a 404, and it costs no extra request. - Both creation paths give the pond a start page, titled from the creator's stored locale. It happens after the creating transaction commits: the owner's grant is written inside it and permissions cache per pond, so creating the page any earlier would ask about rights the grant has not published yet. A failure is logged, not fatal — a pond without a start page still works. `PagesModule` imported `PondsModule` without using it. Removing that vestigial edge let PondsModule depend on PagesModule in the honest direction instead of tying the two together with forwardRef. Every pond created through the api now owns a page, which broke eight suites whose teardown deleted ponds directly — `Page.pond` deliberately has no cascade, because a real purge removes contents explicitly and audits it. A shared `deletePondsWhere` helper deletes pages first. Two tests that counted pages now account for the start page rather than pretending the pond began empty. |
|||
| 5a4a99196e |
#300: route icon-only controls through IconButton/IconLink
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m36s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Build container images (pull_request) Successful in 3m51s
CD / Build and push images (push) Successful in 15s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m32s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m25s
CI / Import/export fidelity gate (push) Successful in 58s
The notification bell sat higher and larger than search and the theme toggle next to it. The cause was not the glyph: `.notifications-bell__button` carried its own rules with neither flex centring nor an icon size, so the svg was laid out inline on the text baseline and rendered at lucide's 24px default instead of the 1.15rem the shared `.icon-button` enforces. Route every icon-only control through the shared components instead: - `IconLink` joins `IconButton`, sharing one class helper. Three controls navigate (pond settings, graph, trash) and are links, not buttons — without a link twin they would have stayed the one group gluing the class on by hand. - 17 hand-applied `className="icon-button …"` usages across nine files now go through the components, which is what enforces the accessible name on a control that shows only an icon. - The bell's unread count reaches assistive technology. The badge sits inside the control, so `aria-label` hid it and a screen reader announced "Notifications" without ever saying how many. An ESLint rule keeps it that way: `icon-button` on a raw button, anchor or Link is now an error, in both string and template-literal form. The plugin uninstall button keeps a title that differs from its name (it explains why a required plugin is locked); IconButton spreads rest last, so the explicit title still wins. Also drops the graphify block from CLAUDE.md — it duplicates the workspace-level instructions. |
|||
| 1f56f34113 |
#296: remove the unsubscribe-token dual-verify window early
All checks were successful
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
Release / Build release images and notes (push) Successful in 3m31s
Release / Release-candidate operations QA (push) Successful in 46s
CI / Build container images (push) Has been skipped
Prod deploy / Deploy the released images to Prod (push) Successful in 58s
CI / Import/export fidelity gate (push) Successful in 59s
CI / Lint, typecheck, test (push) Successful in 6m40s
CI / Auth e2e pack (push) Successful in 8m21s
Restore drill / Restore the latest backup into a scratch stack (push) Successful in 1m18s
CI / Build container images (pull_request) Successful in 2m53s
CI / Auth e2e pack (pull_request) Successful in 8m34s
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 14s
Operator decision at the ADR 0020 acceptance: verification is subkey-only now instead of waiting for the stated 2026-11-01 expiry. Links in digest mails sent before the #188 key separation stop working; recipients use the in-app notification settings. A regression test pins that the legacy derivation (root key + purpose prefix) can never verify again; security.md records the removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 9b7acab294 |
#232: plugin allowlist with SHA-256 hash pinning
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m21s
CI / Build container images (pull_request) Successful in 3m59s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m27s
CI / Import/export fidelity gate (push) Successful in 58s
CI / Lint, typecheck, test (push) Successful in 6m30s
The install path records the SHA-256 of the delivered bundle ZIP (plugins.bundle_hash; pre-#232 installs show it as unknown until reinstalled). plugins.allowlist in instance_settings names permitted ids with their pinned hashes: empty (default) = not enforced, existing instances unchanged; non-empty = installs of unlisted or deviating bundles are rejected (plugin_not_pinned / plugin_hash_mismatch, 403), and an installed plugin outside the list or with a deviating hash does not load — absent from pond mount lists, frame/assets 404. Every rejection is audited (plugin.rejected, catalogue v1.5). A version bump changes the hash and therefore requires an explicit re-pin — the intended friction (ADR 0025). Admin UI shows observed vs pinned hash per plugin with pin/re-pin/unpin. Scope stated honestly in plugin-architecture.md: the pin answers "is this the reviewed bundle"; post-install disk tampering is platform integrity (ADR 0019), sandbox containment stays the sandbox's job. Hardening guide row + catalog advisory triage; residual risk R-03 resolved. e2e: empty-allowlist compatibility, pinned load, unpinned and tampered installs rejected and audited, pin drift blocks loading while the admin still sees the mismatch, version bump needs re-pin. Full api suite 101 files / 561 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 404a3741c8 |
ADRs 0019-0027: accepted after explicit operator review (2026-07-31)
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m34s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CI / Lint, typecheck, test (pull_request) Successful in 6m19s
CI / Build container images (pull_request) Successful in 1m14s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m24s
CI / Import/export fidelity gate (push) Successful in 59s
Stefan reviewed and accepted all nine VS-NfD ADRs one by one. Two adjustments from the review: ADR 0021 decision 3 now states the #216 refinement in the decision itself (PAT/feed-token issuance stays available to IdP-authenticated sessions — API authorization under its own switches, not interactive sign-in) instead of contradicting the later Decisions section; and the ADR 0020 dual-verify window will be removed early (issue #296) rather than waiting for its stated expiry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 4d9f913845 |
#246: mode enforced — reject profile-violating configuration writes
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m18s
CI / Build container images (pull_request) Successful in 4m3s
CI / Auth e2e pack (pull_request) Successful in 8m43s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 23s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Deploy to Test (push) Successful in 12s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m27s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m24s
CI / Import/export fidelity gate (push) Successful in 58s
In enforced mode the ONE settings write path every caller uses rejects catalog-violating values with the stable code vs_nfd_profile_violation (403 — the request is well-formed, the policy says no). Existing violating values are reported at startup (log line, database-less boots must not fail) and on the admin card, never auto-changed. The UI renders as in hidden (#245 already keys on hidden|enforced). The hardening guide now names enforced as the recommended mode for VS-NfD reference operation. Tests: violating write rejected with the stable code and nothing stored; compliant writes pass; the same violating write passes in marked and hidden (own app boots); pre-existing violation reported and untouched. Full api suite 100 files / 555 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 0d95e1304e |
#245: mode hidden — hide profile-violating options, mark the hiding
All checks were successful
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m34s
CI / Import/export fidelity gate (push) Successful in 1m1s
CI / Build container images (pull_request) Successful in 1m13s
CI / Lint, typecheck, test (pull_request) Successful in 6m14s
CI / Auth e2e pack (pull_request) Successful in 8m31s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
In hidden (and later enforced) mode, catalog-listed controls whose only purpose is enabling a violation are not rendered while their saved value is compliant (the four master switches, the Nextcloud backup block); value-listed selects keep only their compliant choices (registration mode, new-page classification, upload policy, SVG policy). Every affected section shows one accessible policy note (i18n de+en) so policy is distinguishable from missing features. A value that was already violating is surfaced exactly like in marked — never silently hidden. The API stays unchanged; enforcement is #246. e2e: hidden half of the marking pack (rows disappear, note visible, already-violating row stays marked, axe WCAG A/AA clean) — verified live locally; CI runs it against a second api (VS_NFD_MODE=hidden, same database) behind its own static server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 5fdef95f67 |
#244: mode marked — flag profile-violating configuration in the UI
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m34s
CI / Build container images (pull_request) Successful in 1m19s
CI / Auth e2e pack (pull_request) Successful in 8m23s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m49s
CI / Import/export fidelity gate (push) Successful in 58s
Every catalog-listed control on the admin surfaces carries an accessible deviation marking in mode marked: text + icon under the control (never colour alone), part of the control's accessible description (aria-describedby), i18n de+en. The check runs against the CURRENT control value, so a violating choice is marked before saving. Covered controls: registration mode, new-page classification, upload policy, SVG policy, the four master switches (api/mcp/feeds/plugins), the legal texts (violating while empty), and the Nextcloud backup toggle on the system panel. The profile card (#243) gains the warning summary and the hardening-guide reference. e2e: new vs-nfd-marking pack (marked half in CI — the e2e api now runs VS_NFD_MODE=marked, which also puts the marked state into the a11y admin scan; off half in local default runs; both halves verified live). hidden/enforced follow in #245/#246. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| da5fd7c770 |
#243: VS_NFD_MODE and the machine-readable hardening-profile catalog
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m12s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 8m29s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Build and push images (push) Successful in 31s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m29s
CD / Promote to Int (push) Successful in 14s
CI / Build container images (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 6m30s
CI / Auth e2e pack (push) Successful in 8m6s
CI / Import/export fidelity gate (push) Successful in 57s
The deployment declares through VS_NFD_MODE (off | marked | hidden | enforced, default off) how the application treats configuration that violates the VS-NfD reference profile — deploy-level like BACKUP_ALLOWED_TARGETS, so a compromised Site Admin cannot widen it. The catalog in shared (vs-nfd-profile.ts) is the single source of truth: every profile-relevant setting with a decidable compliant value, judgement calls in an explicit advisory list, and a fence test parsing the hardening guide's reference tables so neither can drift (pattern #201). The api evaluates the catalog against the typed settings registry and validated env and exposes mode + verdict on GET /admin/system/vs-nfd-profile; the admin settings view shows the card whenever the mode is not off. Display only — the treatments land with #244–#246 (ADR 0027, proposed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 18239e2fa9 |
#221: offline update path incl. migrations, rehearsed with rollback
All checks were successful
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m19s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m13s
CI / Import/export fidelity gate (push) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 6m20s
CI / Build container images (pull_request) Successful in 1m12s
CI / Auth e2e pack (pull_request) Successful in 8m24s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 22s
Adds docs/operations/update-runbook.md (obtain, verify by digest, back up, apply, verify, roll back) with the migration behaviour stated explicitly: a failed migration rolls back its own transaction but is recorded in _prisma_migrations and blocks every further migrate deploy (P3009) — including a re-deployed old image — until migrate resolve --rolled-back; semantically irreversible migrations have exactly one way back, the pre-update backup set. No rolling updates on a compose stage. Rehearsed in the isolated environment of #220: regular update to a v2 image set, then a deliberate failed-update (P3018 division by zero, schema change proven rolled back) with image-rollback-alone shown insufficient and the documented recovery executed. Protocol: docs/vs-nfd/98-update-rollback-protokoll.md. ADR 0024 decisions 5+6 recorded as executed; operations handbook and restore runbook updated; plan checkbox P1-3 ticked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| ccffcaadd6 |
#220: protocol of the isolated deployment run
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m16s
CI / Build container images (pull_request) Successful in 1m26s
CI / Auth e2e pack (pull_request) Successful in 8m27s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m22s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m12s
CI / Import/export fidelity gate (push) Successful in 57s
Full deployment exercised in a compose stack whose networks are all internal: true — setup, login, live collaboration, search, upload, all export formats, backup and restore. tcpdump full capture on both bridges: zero packets leave the isolated subnets; the only outbound attempt the application makes is SMTP, which fails contained in the outbox (5 retries, then FAILED) while the instance stays fully functional. The restore finding became #288, fixed earlier in this chain and re-verified in the same stack. Plan checkbox P1-3 and the I-28 open question ticked; operations handbook airgap section updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 4f6596e8a2 |
#288: reset schema before pg_restore — partitioned tables broke --clean
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m48s
CI / Build container images (pull_request) Successful in 1m46s
CI / Auth e2e pack (pull_request) Successful in 8m22s
CI / Import/export fidelity gate (pull_request) Successful in 1m8s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Since #224 read_events is partitioned; the dump carries per-partition primary keys as own entries, and pg_restore --clean emitted DROP CONSTRAINT against inherited constraints, which PostgreSQL refuses. The restore then reported FAILED although the content was restored. Dropping and recreating the public schema first makes every --clean drop a no-op and the restore faithful: objects created after the backup no longer survive. Verified in the isolated environment of #220 (set 20260731-132200, exit 0, readyz green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| a758c9d78b |
#219: verified reproducible build without network access
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m45s
CI / Build container images (pull_request) Successful in 1m14s
CI / Auth e2e pack (pull_request) Successful in 8m37s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 28s
CI / Lint, typecheck, test (push) Successful in 6m19s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m12s
CI / Import/export fidelity gate (push) Successful in 58s
The ADR 0024 §4 decision, taken explicitly and both ways: customers OPERATE prebuilt digest-pinned images (no customer-side build), and ADDITIONALLY the workspace build is verified to work with networking disabled - so site-local patching stays possible without internet. Evidence (docs/vs-nfd/96-offline-build-protokoll.md): pnpm install --offline --frozen-lockfile plus pnpm build under docker run --network none (node:22.15.1-alpine + pnpm 11.9.0, the pinned toolchain), reproduced twice from clean checkouts with identical results. The offline kit is the pnpm store (~870 MB) plus the build user's ~/.cache (~460 MB - the prisma engines live there; without the cache the prisma postinstall fails offline). The one network dependency found and bounded: the drawio plugin's installable ZIP fetches its pinned vendor tarball on first build. Deploy images contain no plugin ZIPs, so the delivery-relevant build is fully offline (CI=1 skips the fetch, as in CI); an offline ZIP build pre-seeds the tarball into packages/plugins/drawio/vendor/. Also catches up the operations manual's scheduler-job table to 10 (read-trail-maintenance was added in #224 without the row here). Refs #219. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 2f7ba65eef |
#218: mirror procedure into an internal registry
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m11s
CI / Build container images (pull_request) Successful in 1m24s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Auth e2e pack (pull_request) Successful in 8m49s
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
Airgapped sites pull from their own registry (ADR 0024). The image list
is GENERATED (deploy/scripts/list-images.sh resolves the compose file
incl. the caddy profile) so a mirror can never silently miss a service;
third-party images gain a configurable ${REGISTRY_PREFIX:-} in the
compose file (digest pins unchanged - Docker verifies the same sha256
regardless of which registry serves it), own images keep IMAGE_PREFIX;
no image reference is ever edited per site.
Step-by-step procedure in deploy/stages.md 5b: generate list, copy
digest-preservingly (docker buildx imagetools create; plain
pull/tag/push as the documented fallback - the digest comparison closes
the loop either way), verify the digest in the mirror against the pin,
point the deployment via REGISTRY_PREFIX/IMAGE_PREFIX.
Executed once end-to-end and recorded as assessor-facing evidence
(docs/vs-nfd/95-mirror-protokoll.md): all four third-party images
mirrored digest-identically into a local registry:2, plus
dorfteich-api:v0.12.0 (sha256:576f1646... identical on both sides; the
imagetools stall against the Gitea registry is recorded with its
workaround). Operations manual's airgap section now lists the mirror
part as available.
Refs #218.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
|
|||
| 6aac785841 |
#217: map IdP groups and roles onto the permission model
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m55s
CI / Build container images (pull_request) Successful in 3m0s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m28s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m15s
CI / Import/export fidelity gate (push) Successful in 59s
Declarative instance setting idpMapping.rules turns ID-token claims into pond roles and the site-admin flag on every OIDC login — configuration, not code. Mapped grants travel through the SAME GrantsService path as manual ones (permission cache invalidated, collab access notify fires so live sessions revalidate — asserted by test), never raw rows. Ownership makes precedence explicit: role_grants.origin marks mapped rows, users.is_site_admin_managed marks a mapping-set admin flag. The mapping only creates and revokes what it owns — manual wins: hand-made grants and hand-promoted admins are never revoked by a missing claim (a manual toggle clears the marker and takes ownership). Removal of a claim revokes the mapped grant and the managed flag on the next login. Every mapping-driven change is audited with origin idp_mapping. Failure containment: unknown pond slugs and the last-Pond-Admin protection log-and-skip — a mapping problem must never become a login lockout. Tests drive real OIDC logins against the fake IdP with group claims: grant + working access, revocation incl. notify, manual-wins, managed site-admin promote/demote/hands-off. Documented in permissions.md (own section), ADR 0021, data-model.md and the hardening guide (care rule: same PR). Refs #217. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 13f0311d8e |
#216: hard AUTH_LOCAL_ENABLED switch over every local credential flow
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m55s
CI / Build container images (pull_request) Successful in 4m43s
CI / Auth e2e pack (pull_request) Successful in 9m13s
CI / Import/export fidelity gate (pull_request) Successful in 1m4s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
The deploy-level realization of auth.local.enabled (ADR 0021): FALSE answers 404 on every local credential flow — login, signup, e-mail verification, resend, password forgot/reset/change — enforced centrally in the auth guard via the @LocalCredentialFlow() marker before any session or CSRF logic runs. Deploy-level on purpose: a compromised Site Admin cannot reopen the local path, so the runtime-flip residual risk from ADR 0021 does not materialize (R-02 closed in the risk list). An enumeration fence fails when an auth route is neither marked nor on the reviewed allowlist, so a new credential flow cannot ship unswitched. Stated decisions, each tested: sessions/logout keep working for externally authenticated users; PAT and feed-token issuance stays available (API authorization under its own switches, not interactive sign-in). Bootstrap: complete setup (or SETUP_ADMIN_* pre-seed) before flipping; the api warns at boot when local auth is off with neither OIDC nor proxy auth configured. GET /auth/methods reports local:false and the login page hides the local form and credential links. Hardening guide: the planned auth.local.enabled row moves from 1.3 into the live deploy table with the bootstrap ordering, and the verification checklist gains the login-404 probe. Refs #216. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 4c7f001cab |
#215: trusted reverse-proxy header / mTLS client-certificate path
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m44s
CI / Build container images (pull_request) Successful in 4m42s
CI / Auth e2e pack (pull_request) Successful in 9m15s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
For perimeters that authenticate before the application (ADR 0021 §4). Off unless BOTH AUTH_PROXY_HEADER and AUTH_PROXY_TRUSTED_PEERS are set — nothing about the header is guessed. The peer check runs against the TCP peer address only (a forwarded header is attacker-influenced): a request carrying the header from any other peer is rejected outright and audited as auth.proxy_rejected (catalogue v1.4) — that is a spoof attempt, not a misconfiguration — even when a valid session cookie rides along. From a trusted peer the header IS the identity; a session cookie never escalates beyond it; with the feature off the header is inert. Mapping is explicit (AUTH_PROXY_MAP: username or e-mail); deliberately no just-in-time creation — the header carries no verified address. The mTLS variant (AUTH_PROXY_MODE=mtls-dn) maps the configured attribute (default CN) out of the certificate subject DN the TLS terminator forwards, under the same peer rules. Session-less proxy requests key the read trail per user (user:<id>). The trust boundary is stated in security.md (the section an assessor reads closest), the VS-NfD security documentation and the hardening guide's deploy table. Tests cover all four decisions: off = inert, trusted peer authenticates (username and DN mapping), untrusted peer rejected + audited, no escalation past a session cookie. Refs #215. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 5796b7a5dd |
#214: OIDC Authorization Code with PKCE, Keycloak as reference IdP
External authentication (ADR 0021) built on jose (#188's vetted library) plus fetch — no new dependency enters the supply chain for a security base function. Discovery-configured; ID tokens validate against the IdP's JWKS under an explicit RS256/ES256 allowlist with issuer, audience, expiry and nonce binding. State, nonce and the PKCE verifier travel in a signed HttpOnly Lax cookie keyed by a dedicated HKDF purpose (oidc-state, ADR 0020). Deploy-level configuration (OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES/ PROVIDER_LABEL): who authenticates users is a platform decision. The login page discovers the provider via GET /auth/methods and renders the SSO button (i18n de+en). Identities use the existing slot (provider oidc:<issuer>, subject from the token). First login creates the account just-in-time — ACTIVE and mail-verified only when the IdP asserts a verified address. An existing local account is NEVER adopted silently by e-mail (account-takeover path): login refuses with oidc_link_required and the owner links explicitly via GET /auth/oidc/link (audited auth.identity_linked, catalogue v1.3). Sessions come from the one existing session service. Tests run the full flow against a protocol-faithful fake IdP: PKCE verifier at the token endpoint, JIT creation incl. personal pond, invalid state/nonce/signature/issuer/audience/expiry each rejected, the linking refusal and the explicit link flow. Verified end-to-end against a real Keycloak 26.0 (repeatable procedure documented in security.md §External authentication). Refs #214. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 4af5e6e81f |
#225: read-trail master switch and written purpose limitation
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m2s
CI / Build container images (pull_request) Successful in 4m1s
CI / Auth e2e pack (pull_request) Successful in 8m26s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m23s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m6s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m25s
CI / Import/export fidelity gate (push) Successful in 1m0s
New instance switch readTrail.enabled, default OFF: read logging is employee monitoring in a works council's eyes — an ordinary instance must not surveil reads. Off means no event is written ANYWHERE (no row, no stdout line, verified by test); the api announces the switch position once per boot, so an eventless trail is never ambiguous — a gap reads as "was off", never "was lost". The written purpose limitation ships as section 7 of the VS-NfD security documentation (#228): what is recorded (no content, no titles, no IPs, no fingerprinting), why (evidence for reads of marked content only — variant A is the technical anchor of the promise), who may read it (Site Admin, API-only), for how long (readTrail.retentionDays, audited pruning), and what it may NOT be used for (no performance or behaviour monitoring). The hardening guide's reference configuration turns the trail on (reference value true) and points to that text; the existing trail suites now enable the switch explicitly. Refs #225. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 2bdb0ec2cf |
#224: read-trail storage — partitioning, retention, admin query path
Convert read_events to monthly RANGE partitions on occurred_at, with a DEFAULT partition as safety net: a lagging maintenance job must never turn the trail's hard-failure semantics into an outage for classified reads. The dedup unique pair (#223) moves to per-partition indexes (PostgreSQL cannot carry it on the parent); a bucket spanning a month boundary may record one duplicate — over-recording is acceptable, gaps are not. New daily job read-trail-maintenance (job-count fence 9 -> 10) creates months ahead — each with its dedup index — and applies the trail's own retention readTrail.retentionDays (default 365, deliberately independent of audit.retentionDays): whole expired months are DROPped without scanning, remainders deleted by range, every run audited as read_trail.pruned (catalogue v1.2; the fence regex now admits an underscore namespace). Site-Admin query path GET /admin/system/read-events answers "who read page X" and "what did user Y read" within a period — API-only by design, documented. Growth measured and documented in data-model.md: ~1 MB per 1000 events including indexes. Tests: retention pruning + audited deletion + admin queries on the shared database; the partitioned shape, per-partition P2002 dedup, months-ahead creation and DROP-based pruning against a fresh database built by the real migration chain. Refs #224. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| fd4fd60c99 |
#223: dedup window for the read trail
One event per (session, page, channel) within an aligned window of readTrail.dedupWindowMinutes (default 5): buckets are floor(epoch / windowSeconds), and a unique (dedup_key, window_bucket) pair collapses concurrent duplicates race-free at insert time — the first access in a window is always recorded, a later duplicate lands on the unique violation and is skipped quietly (a skipped duplicate is not a gap; only real write failures still abort the read). Each row carries windowSeconds, so the evidence states it represents a window, never a request count. Reconnects within a window stay one event; a new session records again even for the same user; channels never collapse into each other; the page-less attachment key uses the documented `-` placeholder. Load evidence: 30 collab-token renewals inside one window produce exactly one event (test), bounding a live editing session at ~12 events/hour/page. Window semantics documented in ADR 0023, the VS-NfD security documentation (#228) and as a hardening-guide line for the new setting (care rule: same PR). Refs #223. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 05a979bac3 |
#222: read-access trail for classified pages
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m25s
CI / Build container images (pull_request) Successful in 2m58s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
Instrument every full-content read channel for pages with classification = vs_nfd (ADR 0023, variant A): SPA state fetch and read rendering, public JSON content, no-JS shell, expanded embeds, public API GET (incl. the MCP read_page path and write echoes), attachment download under the #212 effective classification, all export shapes (markdown, pond ZIP, account data export, queued docx/odt/pdf at enqueue), and collab-token issuance as the api-side proxy for the WS join. Events land in the new read_events table (no FKs — evidence survives page purges and hard user deletions) with actor, session key (session:/token:/job:/anon), page, pond, channel and the classification at read time. Recording failures are NOT swallowed: a failed write aborts the read (hard failure, the deliberate contrast to AuditService — decision recorded in ADR 0023 and security.md §Logging, together with the recorded residuals: content fragments and feeds). One e2e test per channel proves both the event and its absence for unclassified pages, plus the hard-failure semantics. Refs #222. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 919201d9b0 |
#230: IT-Grundschutz mapping for APP.3.1 and CON.11.1
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m56s
CI / Build container images (pull_request) Successful in 1m23s
CI / Auth e2e pack (pull_request) Successful in 8m57s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 22s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 5m48s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m13s
CI / Import/export fidelity gate (push) Successful in 58s
docs/vs-nfd/80-grundschutz-mapping.md against the Edition 2023 texts of both building blocks (fetched from the BSI single PDFs, edition and retrieval date stated; the dropped requirements of APP.3.1 are listed as such, CON.11.1's 18 requirements are all Basis). Every requirement classified as product / operator / n.a.: product rows point at code, configuration and tests (auth+rate limits, upload controls, security headers with the honest HSTS-at-the-proxy split, marking = the whole of M26 under CON.11.1.A7 incl. the answered does-the-marking-carry-a- security-function question); operator rows say what we hand over (copy list, procedures, network plan, SBOMs); n.a. rows are argued via the delimitation statement (no §52 security functions, no built-in remote maintenance). Open requirements point at their closing issues (M27/M28/M29/M32), so the document doubles as the gap list; the never-scheduled external pentest is stated honestly. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 87c1c5ee88 |
#231: residual-risk list
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 7m1s
CI / Build container images (pull_request) Successful in 1m16s
CI / Auth e2e pack (pull_request) Successful in 8m48s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
docs/vs-nfd/90-restrisiken.md: nine entries, each with risk, why it is accepted, compensating control and decider — unmarked attachment content (#212), local auth not yet switchable incl. the open runtime- flippability question (#216), deferred plugin hash pinning (#232), the one-time git-history secret check with its pattern caveat (#198), digest-mail titles (I-23, revisit M32), page_links slug residue (I-24), the IndexedDB endpoint copy (I-25), deliberately unscheduled features, and the Site-Admin read bypass. Binding same-PR maintenance rule stated; referenced from the delimitation statement and consumed by the Grundschutz mapping. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 2c6eff85f3 |
#227: hardening guide with the VS-NfD reference configuration
docs/vs-nfd/50-haertungsleitfaden.md: one adoptable profile — every entry with the exact switch name, value, default and the reason, split into instance settings (registration closed, api/mcp off, feeds off, plugins off, classification defaults vs_nfd + upload block, svg reject, minimal extension list) and deploy-level configuration (empty BACKUP_ALLOWED_TARGETS enforces backup-local-only outside Site-Admin reach; tightened session hours; SMTP deliberately unconfigured with the consequence stated honestly). auth.local.enabled is listed as the one pending row (#216) with its compensation until then; the guide states the binding updated-in-same-PR rule for every future switch. Includes an operator verification checklist (four unauthenticated 404 curls + readyz + admin spot checks). Cross-referenced from the delimitation statement (file names made concrete) and consumed by the Grundschutz mapping (#230). Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 040f3fbeae |
#229: operations manual (install, update, backup/restore, deletion, roles)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 7m1s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 8m47s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
docs/vs-nfd/70-betriebshandbuch.md: installation as run on the real stages (airgap variant explicitly pending #218-#221 with what already exists as groundwork), update/rollback incl. the no-down-migrations caveat, backup/restore with the ADR-0026 target allowlist and the rehearsed monthly restore drill (evidence: logs on #98), the full scheduler-job table (cadences verified against code), the deletion-and- destruction chapter built on the #228 copy list (per content type: what deletion reaches, what remains, immediate-destruction path, decommissioning), and role separation incl. the deliberate limits of a Site Admin and the honest note that Site Admin read-bypass makes the content/platform split non-absolute app-side. Every procedure carries its evidence level (erprobt / nicht geprobt / offen) — nothing claimed above what was actually executed. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| f0c6af4412 |
#228: security documentation (architecture, data flows, network plan)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 7m0s
CI / Build container images (pull_request) Successful in 1m22s
CI / Auth e2e pack (pull_request) Successful in 9m2s
CI / Import/export fidelity gate (pull_request) Successful in 1m2s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
docs/vs-nfd/60-sicherheitsdokumentation.md: component diagram with per- service purpose and privileges, network plan digit-exact against the deploy compose (127.0.0.1-only app bindings, internal-only data zone), data-flow diagrams (auth, realtime editing incl. LISTEN/NOTIFY and the 60s collab token, export via the pinned sidecars, backup incl. the ADR-0026 allowlist, and every read channel), named trust boundaries (reverse proxy, plugin sandbox, outbound SMTP/mirror), and the complete list of content copies — in-database, on-volume and outside the instance — that the deletion concept in #229 builds on. Mermaid only, German (assessor audience), with the maintained-in-same-PR rule stated. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 868b79c8bc |
#213: warn on uploads to classified pages; instance policy can block
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
The attachments panel of a classified page shows a persistent notice naming the consequence (de+en): the file inherits the page's classification but its content carries no marking (#212). The new instance setting classification.uploadPolicy (default warn, documented; the VS-NfD reference configuration blocks, #227) hardens the warning into a server-side rejection (403 classified_upload_blocked) — enforced in the upload service, not only in the UI. Tests: warning visible in the local attachments pack; block enforced server-side with warn/block both ways and open pages unaffected. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| e505fc74dc |
#212: mark attachment downloads by filename prefix and companion file
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m34s
CI / Build container images (pull_request) Successful in 14s
CI / Auth e2e pack (pull_request) Successful in 9m36s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Downloads whose effective classification is vs_nfd carry the documented VS-NfD_ filename prefix (single source classificationFilenamePrefix() in shared; ADR 0022 records the short form for file names). Effective classification: the linked page's level; an attachment with unset pageId (paste-then-insert, pond-level) FAILS CLOSED to the highest level of any live page in its pond. The pond export ZIP adds a sibling <file>.classification.txt companion with the full marking for classified media, next to the manifest entry (#210). Documented in operations.md, incl. the deliberate residual risk: the file's own content carries no marking (recorded on #231, not hidden). Tests: prefixed classified download, unchanged open download, fail-closed orphan both ways, ZIP companion + manifest level. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 521ea514b4 |
#211: classification through feeds, public API, search and the no-JS shell
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m38s
CI / Build container images (pull_request) Successful in 4m14s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Feeds: classified entries carry a standard Atom <category> (term=level, scheme=urn:dorfteich:classification, label=the fixed wording); the feed document states the highest contained level once; all-open feeds carry none. Public API: page representations (list+get) gain the classification field, OpenAPI + public-api.md documented. Search: every hit carries the level and the palette renders the marking with the snippet (compact form of the banner, text token only). No-JS shell: banner above and below the content, own markup for the separate render path; unclassified pages unchanged everywhere. One test per channel (feed categories + count, public API list/get with the switch on, search hit levels, shell top+bottom). Also: fidelity CI sidecars get per-job container names — the fixed names collided across parallel runs on the shared host (run 547's red fidelity job; a fixed-name cleanup could even kill a sibling's live sidecars). Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 68497046e9 |
#210: mark the Markdown ZIP export with frontmatter, imprint and manifest
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m1s
CI / Build container images (pull_request) Successful in 2m58s
CI / Auth e2e pack (pull_request) Successful in 9m6s
CI / Import/export fidelity gate (pull_request) Successful in 1m8s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 24s
CI / Lint, typecheck, test (push) Successful in 6m37s
CD / Deploy to Test (push) Successful in 12s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Has been cancelled
A classified page's .md carries the level in YAML frontmatter AND the marking line at top and bottom; unclassified files are byte-identical to before. Every pond archive (incl. the per-pond folders of the account data export) ships a manifest.json listing each file with its level and stating the highest level once at archive level — media inherits the highest classification among the readable pages referencing it (fail-closed). Round trip: the importer recognizes exactly our frontmatter block, strips it plus the imprint lines, and creates the page at least at the imported level (content must not escape its marking by traveling through a ZIP) — pinned by unit and e2e round-trip tests. Foreign frontmatter passes through unchanged; the Obsidian vault import keeps its own frontmatter modes. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 74a9e495e4 |
#209: pandoc reference documents carry the VS-NfD marking for DOCX/ODT
reference-vs-nfd.docx/.odt ship as derived binaries: the pinned pandoc's
default reference documents plus a header and footer with the marking —
part of the document's page setup, so it repeats on every page in Word
and LibreOffice and is not deletable body text. Source of truth is
scripts/gen-classified-reference-docs.mjs (wording from shared
classificationMarking(); maintenance documented in assets/README.md).
The converter passes reference docs to pandoc-server via in-request
files + reference-doc; the worker attaches them for marked docx/odt jobs
(job option {marking}, as in #208). Unclassified exports pass nothing
and are unchanged (pinned by fake-converter test). Fidelity suite
asserts against real pandoc 3.6 that marked outputs carry the
header/footer parts and unmarked ones do not; per-page repetition
verified via LibreOffice 25.8 headless PDF (5/5 pages, 2 markings each,
both formats). Word: quick manual look pending (sample files in the
workspace), procedure documented in assets/README.md.
Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
|
|||
| c2df7c0c23 |
#208: VS-NfD marking in the Gotenberg per-page header and footer
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m16s
CI / Build container images (pull_request) Successful in 3m0s
CI / Auth e2e pack (pull_request) Successful in 8m54s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Successful in 24s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m27s
CI / Lint, typecheck, test (push) Failing after 6m24s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
A classified page's PDF export carries its marking as a job option; the renderer hands it to Gotenberg's Chromium header/footer templates, so it repeats on every page — bold centered in the running header and next to the existing page numbers in the footer. Unclassified pages send exactly the pre-#208 forms (unchanged PDF, asserted by the fidelity smoke and a lastMarking=null check). New real-Gotenberg fidelity test asserts the marking appears twice on EVERY page of a multi-page render while the document-level header keeps working. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 809e071f14 |
#207: print stylesheet with the classification on every printed sheet
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m56s
CI / Build container images (pull_request) Successful in 1m27s
CI / Auth e2e pack (pull_request) Successful in 9m18s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m5s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
First @media print support at all: page size/margins, navigation and interactive chrome suppressed, break behaviour for headings, tables, code blocks, figures and plugin blocks. The VS-NfD marking runs as header AND footer on every sheet via a real-table PrintFrame whose thead/tfoot browsers repeat per page — @page margin boxes are unimplemented and position:fixed places unreliably in both engines (verified empirically); on screen the table chain renders as plain blocks, so nothing changes visually. Verified as PDF-from-browser in Chromium 140 and Firefox 153 (2 markings on every page of a multi-page document); the repeatable procedure is documented in apps/web/e2e/README.md. Unclassified pages print without a marking. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| adceca7358 |
#206: show the VS-NfD marking in web view header and footer
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CI / Build container images (pull_request) Successful in 4m24s
CI / Auth e2e pack (pull_request) Successful in 8m44s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 26s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m30s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m55s
CI / Import/export fidelity gate (push) Failing after 50s
ClassificationBanner renders the fixed ADR-0022 wording above and below the content in reading view, editor and public page view; unclassified pages show nothing. Announced to assistive tech via a localized hidden prefix (de+en); styled from the plain text token only, so contrast holds in both themes and under every accent with no new color pair. Public content endpoint now carries the classification. New seed fixture classified-note; a11y pack asserts banner top+bottom and axe-clean in light and dark. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 488d0d06f1 |
#205: classification inherits down the tree; lowering is a guarded, audited act
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m40s
CI / Build container images (pull_request) Successful in 4m34s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
New pages take max(instance default, parent level); moving a subtree under a higher-classified parent raises every member below that level. No move-like path (reposition, trash-promote, purge-promote) lowers a level as a side effect — pinned by test. Raising is ordinary editorial work; lowering requires the dedicated capability canLowerClassification (pond-wide Pond Admin) in the central permission model. Both directions are audited (page.classification_raised/_lowered, catalogue v1.1) with old value, new value, actor and page. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 183faf7710 |
#204: classification as first-class page metadata (ADR 0022)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m42s
CI / Build container images (pull_request) Successful in 3m56s
CI / Auth e2e pack (pull_request) Successful in 8m17s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 24s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 6m17s
CD / Smoke tests against Test (push) Successful in 3m32s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 8m20s
CI / Import/export fidelity gate (push) Successful in 55s
Enum field on Page (UNCLASSIFIED default, VS_NFD), migration backfills existing pages. New pages take the instance-wide default from classification.newPageDefault (admin-visible, de+en). The value rides in every PageView, so no channel needs an extra request. The field is a marking, not a protection mechanism: a test pins that permission decisions are unchanged by it. The marking wording is fixed in ADR 0022 and sourced solely from classificationMarking() in @dorfteich/shared. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| db4f517e44 |
#203: pin all third-party deploy images by digest
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m32s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 8m22s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 56s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 52s
CI / Lint, typecheck, test (push) Successful in 5m36s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m3s
CI / Import/export fidelity gate (push) Successful in 57s
The four third-party images in the deploy compose (postgres, pandoc, gotenberg — previously a floating MAJOR tag —, caddy) are now name:tag@sha256 pins; the tag stays for readability, the digest decides what runs. The pinned digests are exactly what the stages already run (verified against the live containers' RepoDigests on ONE), so the next recreation is byte-identical. A new early CI step fails on any third-party compose image without a digest; compose.dev.yml is a local convenience and deliberately exempt (its node helpers now follow the #236 pin). Update + rollout procedure in deploy/stages.md — CD does not sync stage composes, so the hand rollout to test/int/prod is part of this issue's definition of done. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ |