Skip to content

Releases: MSK-Scripts/mskanban

v0.6.0-beta

30 May 17:42

Choose a tag to compare

v0.6.0-beta Pre-release
Pre-release

Completes the entire 25-point "promised-vs-built" gap backlog (#27#51) on top
of v0.5.0-beta. Auth hardening (per-user login backoff + per-user API
rate-limit, opt-in HaveIBeenPwned check, ReDoS guard, DB-enforced append-only
audit log); transactional security email (account-lock alert + opt-out
login-notifications, fail-open SMTP); sanitized Markdown rendering; the
user-picker custom field; board templates; three extra importers (GitHub
Projects / Jira CSV / Wekan); attachments end-to-end E2EE; GDPR export +
crypto-shred erasure; a metadata-only admin area; a public REST API via
Personal Access Tokens; a Mind-Map board view; automated WCAG 2.1 A/AA axe
scans; table-view virtual scrolling; live CRDT sync for checklists +
comments
(ADR 0020); and offline-first — precache cold-start + a
card-scoped offline write outbox (ADR 0021). New ADRs 0016–0021. Still
zero-knowledge end to end: the server only ever sees opaque enc_* envelopes.

Security

  • Transactional security emails: account-lock alert + login notification
    (#32, ADR 0018). A new
    nodemailer/SMTP mailer (src/lib/mail/) sends two security emails: an
    account-lock alert whenever repeated failed logins trip the lockout
    (always sent; no IP, since the attempts may be the attacker's), and an
    opt-out login notification on a sign-in from a new device/IP (detected
    via Session.ipHash with no prior session). The notification carries
    IP + User-Agent + time (all HTML-escaped against a hostile UA) and is
    governed by User.loginNotifications (default ON), toggleable on the
    account page (GET/PATCH /api/account/preferences). The mailer is
    fail-open: with no SMTP_HOST configured it is a silent no-op, and a
    transport error never propagates into the auth flow — a stock instance
    sends nothing and never crashes. No SaaS, no telemetry, no third-party
    IP leakage; geolocation is intentionally out of scope (§16.8). Email
    bodies carry only metadata, never card content or key material. Migration
    20260530000001_login_notifications.
  • Opt-in HaveIBeenPwned compromised-password check (#29,
    ADR 0017).
    Registration and password-change can reject passwords found in known
    breaches via a client-side k-anonymity lookup — the password never
    leaves the device; only the first 5 hex chars of its SHA-1 digest are
    sent to api.pwnedpasswords.com. Gated behind
    NEXT_PUBLIC_FEATURE_HIBP_CHECK (default OFF), so a stock or
    air-gapped instance still makes zero third-party calls (§16.8). The CSP
    connect-src opens the range API only when the flag is on, and the
    lookup fails open (a HIBP outage never blocks the user).
  • Per-user API rate limit on every authenticated request (#28).
    requireSession() — the choke point every authenticated route already
    passes through — now applies a per-user sliding window
    (RATE_LIMIT_API_PER_MIN, default raised to 600/min ≈ 10 req/s, well
    above any human burst) as an abuse backstop on top of RBAC + the per-IP
    auth limits. It fails open if Redis is unreachable, so a cache blip
    can never lock users out of the app. The previously-unused
    RATE_LIMIT_AUTH_PER_MIN / RATE_LIMIT_API_PER_MIN env vars are now
    actually read (login per-IP budget + the per-user limiter respectively).
  • Per-user exponential login backoff is now wired (#27). The
    backoffMs() curve (1, 2, 4, 8, 16, 30, 60 s → 5-min cap) existed but
    was dead code; the login path only had a per-IP fixed window + the hard
    account lock. Failed logins (wrong password or wrong second factor)
    now arm a Redis-clocked cool-off keyed by the user, so it follows a
    targeted account across rotating IPs; the next attempt is rejected with
    429 Retry-After until it expires, and a successful login clears it.
  • AuditLog is now append-only at the database level (#31). A
    BEFORE DELETE trigger (migration 20260529000000_auditlog_append_only)
    rejects any attempt to delete an audit row, so history can't be erased
    even outside the app. (Single-statement trigger so Prisma's migration
    runner applies it cleanly; UPDATEs stay convention-enforced because the
    only legitimate one is the GDPR userId → NULL de-association.)
  • Two previously-dead audit actions are now emitted (#31):
    TWOFA_VERIFY_TOTP / TWOFA_VERIFY_WEBAUTHN on a successful second
    factor at login, and RECOVERY_USE when a valid recovery proof unseals
    the challenge. (EXPORT_REQUEST / ACCOUNT_DELETE remain declared for
    the planned DSGVO endpoints, #46.)
  • ReDoS guard (#30). An ESLint no-restricted-syntax rule now bans
    new RegExp() / RegExp() so a dynamic, user-influenced pattern can't
    be introduced without a conscious eslint-disable + safe-regex review.
    The codebase has no dynamic regex today (all patterns are static
    literals); this keeps it that way.

⚠️ Migration required. Run pnpm prisma migrate deploy (prod) /
pnpm prisma migrate dev (local) for the new append-only trigger.

Removed

  • 5 unused dependencies + dead MSW skeleton (docs-truth audit, Phase A). A
    deep "promised vs. actually built" audit found five runtime/dev dependencies
    that were installed but never imported anywhere in src/:
    @tanstack/react-query (server-state is plain fetch + Zustand),
    bullmq (background jobs are in-process setInterval schedulers, not a
    queue), socket.io + socket.io-client (the WS relay uses native ws), and
    iron-session (sessions are the self-written cookie+DB scheme). All five were
    removed. The never-wired MSW test skeleton (tests/msw/, a /api/health stub
    from Phase 2 that no test or setupFiles ever imported) was deleted and msw
    dropped from devDependencies. No behavioural change — pure cleanup.

Changed

  • Documentation now matches the code (audit Phase A). Corrected CLAUDE.md
    §2 tech-stack (self-written auth, not Auth.js/NextAuth; native ws, not
    socket.io; in-process schedulers, not BullMQ; libsodium crypto_pwhash, not
    argon2-browser), completed the OpenAPI spec (now v0.6.0, ~70 routes —
    added the previously-undocumented Milestones, Automation, and
    account/auth-settings routes; the old "56 routes" figure was wrong), clarified
    that /api/auth/2fa/verify is not a standalone endpoint (verification runs
    inside /api/auth/login), and made several overstated claims honest in
    docs/threat-model.md + CLAUDE.md: exponential login backoff is prepared but
    not yet wired, safe-regex/HIBP/login-notification-emails are planned (not
    built), Markdown is currently shown as escaped plaintext (no render → no
    DOMPurify sink yet), offline mode is read-only + per-description drafts (no
    Workbox / write sync-queue), live cursors sit behind a default-off flag, and
    style-src still uses 'unsafe-inline' (only script-src is nonce-based).
    No code behaviour changed; this is the truth-in-docs pass that precedes the
    feature backlog.

Added

  • Offline-first: cold-start precache + offline write outbox (#38,
    ADR 0021). The hand-rolled
    service worker now precaches the build's hashed JS/CSS via a manifest
    generated during pnpm build (scripts/gen-precache-manifest.mjs
    public/precache-manifest.json, versioned by the Next build id), so an
    installed PWA cold-starts fully offline (cache-first for immutable
    /_next/static/, network-first for navigations, /api/ never cached). No
    Workbox dependency. Plus a card-scoped write outbox: while offline, the
    idempotent card mutations (move/reorder, metadata + archive, checklist-item
    toggle) are queued in IndexedDB (src/lib/idb/outbox.ts) and replayed in
    order on reconnect
    (src/lib/offline/sync.ts, driven by the new
    OfflineSync header pill). The server stays authority — a permanent
    4xx drops the queued change (last-write-wins) and surfaces a toast; a
    transient error keeps the tail for the next attempt. Because only
    naturally-idempotent, no-return-value ops are queued, no server route
    changed and no idempotency key is needed
    . The outbox stores ciphertext +
    metadata only
    (request bodies are already enc_* envelopes) and is wiped
    on logout. Shared IndexedDB handle extracted to src/lib/idb/db.ts (DB v2).
    Tests: tests/unit/outbox.test.ts (+7). Deferred (documented): offline
    create (card/comment) + comment-edit need client-generated ids /
    temp-id reconciliation — a follow-up increment.
  • Live CRDT sync for checklists + comments (#39,
    ADR 0005 +
    ADR 0020). With a
    card open, checklist and comment changes from other members now appear
    live (no SSE-tick + full refetch) and concurrent structural edits
    (two people adding items, toggling different items, posting comments at
    once) merge conflict-free. Implemented as a live overlay: three
    id-keyed Y.Maps ride the card's existing Y.Doc over the same
    encrypted WS relay the description uses (no server/relay change), while
    REST stays the durable authority and the only side-effect trigger —
    so RBAC, the append-only audit log, automation emitters
    (comment_added/append_checklist/post_comment), @-mentions,
    notifications and the DSGVO export are all untouched. Comment deletes use
    a tombstone (no resurrection; authorship is security-relevant); checklist
    structure drops keys. Map values are plaintext only inside the Y.Doc
    (AEAD-encrypted on the wire, wiped from IndexedDB on logout — exactly as
    the description draft already works); the server still sees only opaque
    envelopes. New src/lib/realtime/card-collab.ts (pure view-merge
    builders + CardCollab controller, unit-tested incl. a two-peer sync
    case); the card drawer r...
Read more

v0.5.0-beta

28 May 19:58

Choose a tag to compare

v0.5.0-beta Pre-release
Pre-release

Closes the four headline features that were documented but never actually
built — account recovery, workspace member management, WebAuthn /
passkeys
, and public read-only boards — and lands a two-pass
security/correctness audit that fixed 20 bugs (two of them critical
user-facing breakages on reload, plus an SSRF, a cross-tenant IDOR, and a
zero-knowledge logout leak). The whole repository was also reformatted with
Prettier.

⚠️ Migration required. Public boards add three Board columns —
run pnpm prisma migrate deploy (prod) / pnpm prisma migrate dev (local).
Migration 20260528000001_public_boards is committed.

Added

  • Account recovery flow (ADR 0012). The 24-word recovery phrase generated at
    sign-up was previously write-only — a forgotten password meant permanent
    lockout. There is now a real, zero-knowledge recovery: POST /api/auth/recovery/init returns the encrypted key material plus a
    proof-of-possession challenge sealed to the account's X25519 public key;
    only a holder of the phrase can walk phrase → recovery key → User Symmetric Key → private key to unseal it. POST /api/auth/recovery verifies that proof
    (single-use, 5-min, bound to the user id), then rotates the password-derived
    material (authHash + kdfSalt + encSymmetricKey), revokes all sessions,
    and logs the user straight in. The keypair and recovery backup survive intact,
    so every sealed Workspace Key keeps working. New (auth)/recover page; covered
    by tests/unit/recovery-flow.test.ts. ⚠️ Crypto-relevant.
  • Workspace member management. Workspaces were effectively single-user — there
    was no way to add anyone. Now: POST /api/workspaces/{wsId}/members (with a
    Workspace Key the inviter sealed for the invitee's public key),
    POST .../members/lookup (resolve a public key by email, ADMIN+),
    PATCH /.../members/{userId} (change role) and DELETE /.../members/{userId}
    (remove, or leave). RBAC + last-owner protection enforced server-side; new
    MembersPanel on the workspace page. Note: removal does not rotate the
    Workspace Key (the standard shared-key E2EE limitation — a removed member who
    already cached the key keeps what they had; their server access is revoked).
  • WebAuthn / passkeys as a second factor (ADR 0013). Because the password
    derives the Master Key it can't be replaced, so passkeys complement (or stand
    in for) TOTP rather than enabling passwordless login. Enrol/manage passkeys on
    /2fa; at login the server returns TWO_FACTOR_REQUIRED with the available
    methods and the form offers a TOTP field and/or a "Use a passkey" button.
    Routes under /api/auth/webauthn/*; verification via @simplewebauthn.
    ⚠️ RP config (WEBAUTHN_RP_ID / _ORIGIN) must match the deployed domain.
  • Public read-only boards (ADR 0014). Share a board (e.g. a public roadmap)
    via a link without breaking zero-knowledge: enabling sharing (ADMIN+) mints a
    random publicSlug; the BoardKey rides only in the link's URL fragment
    (/p/<slug>#key=…), which browsers never send to the server. The
    unauthenticated GET /api/public/boards/{slug} serves only the board structure
    (columns, non-archived cards, labels, encrypted title) — never members,
    comments, assignees, checklists or custom fields. New viewer at /p/[slug],
    share controls on the board, BOARD_PUBLIC_ENABLE/_DISABLE audit events.

Changed

  • Login second-factor handshake. apiError now forwards AppError.meta, and
    the login route returns 401 { error.meta = { reason: "TWO_FACTOR_REQUIRED", methods } } (no failed-attempt increment for the "no factor yet" step) so the
    client knows which factors to offer. A wrong factor still counts toward the
    lockout threshold.
  • Board export/import round-trips Column.isDone — the "done column" flag is
    no longer lost on export → import (analytics keep working on imported boards).
  • env schema gains NEXT_PUBLIC_WS_URL and AUTOMATION_DUE_WORKER_DISABLED
    (previously read raw from process.env), and WEBHOOK_TICK_TOKEN now requires
    ≥ 20 characters when set.
  • Repository-wide Prettier pass — formatted ~75 pre-existing non-conforming
    files; pnpm format:check is now clean across the tree.

Fixed

  • App dead on reload (two critical AAD bugs). Boards stored every
    workspace/board/column/card envelope bound to :new (the planned post-create
    rebind to :<id> was never implemented), but several read paths decrypted with
    :<id> only and aborted the whole list/board on the first failure. The
    workspaces list, the board list, and — worst — every board (columns are
    always affected) went blank after a reload. All decrypt sites now try
    :new:<id> with per-item resilience.
  • SSRF in the webhook dispatcher (security). Outbound delivery dialled the
    target URL with no runtime guard and followed redirects; the create-time check
    was resolution-free (DNS-rebinding) and missed alternate IP encodings. New
    src/lib/security/ssrf.ts resolves every A/AAAA record and rejects
    private/reserved addresses at send time, and the fetch now uses
    redirect: "manual". Covered by tests/unit/ssrf.test.ts.
  • Cross-tenant label IDOR (security). attach/detach only checked write
    access on the label's board, not that the card belonged to the same board — a
    member could pin labels onto cards in other workspaces. Now verifies same-board.
  • Decrypted content survived logout (zero-knowledge leak). Sign-out was a
    plain server form POST that only cleared the cookie; the client never wiped the
    in-memory key bundle, the IndexedDB plaintext cache, the Yjs drafts, or the
    Master Key session envelope. Logout is now a client action that calls
    reset() (which also mkSession.wipe()s).
  • Webhook double-delivery under multi-worker deployments — the reservation
    CAS changed no predicate field, so two workers could both claim a row. Now
    CAS-es on nextAttemptAt.
  • Same-column downward drag landed one slot too high (the dragged card wasn't
    excluded before computing neighbours).
  • Date off-by-one in negative-UTC timezones — the card-drawer inputs, the
    calendar buckets, and the kanban/table due-date labels mixed local and UTC
    date parts; all now consistently UTC (matching how dates are stored).
  • SSE Redis subscriber leak when a client aborted during stream start.
  • TOTP failures didn't count toward account lockout (only the password path
    did); a wrong second factor now increments + can lock.
  • Comment edit/delete lacked a board-membership check (a removed user with a
    stale session could still edit their old comments).
  • Redis ensureRedis connect race — concurrent first callers could double
    connect(); now memoised.
  • Condition-less automation rules couldn't be retyped (a carried-forward
    null trigger_meta was rejected by the envelope validator).
  • Board-presence broadcast was up to ~10 s late (no re-broadcast after the
    server's joined ack); the card-cursor provider stopped marking itself joined
    before the server authorised the room.

Security

  • Account recovery uses a proof-of-possession challenge so the server never
    authorises a password change without proof the caller recovered the account
    (ADR 0012).
  • SSRF guard, cross-tenant label IDOR, and the logout zero-knowledge leak above
    are all security-relevant fixes.

Full Changelog: v0.4.0-beta...v0.5.0-beta

v0.4.0-beta

28 May 18:05
214f8f2

Choose a tag to compare

v0.4.0-beta Pre-release
Pre-release

Ships live cursors in the card description editor (ADR 0011 — behind
NEXT_PUBLIC_FEATURE_LIVE_CURSORS, end-to-end encrypted), plus two
operator-facing fixes uncovered while debugging a real production deploy:
the Apache vHost example no longer clobbers Next.js's nonce CSP (which had
silently bricked hydration), and the 2FA enrolment screen no longer
dead-ends. Also adds the brand logo and the missing PWA icon set.
⚠️ Switching live cursors on requires redeploying the WebSocket relay from
this version.

Added

  • Brand logo + PWA icons. The auth screens and the app header now
    show public/logo.png instead of a text-only wordmark, and the
    missing PWA icon set (icons/icon-192.png, icon-512.png,
    icon-maskable-512.png, apple-touch-icon.png) is generated from the
    logo — fixing the GET /icons/icon-192.png 404 from the web manifest
    and giving installs a real icon. apple-touch-icon wired into the
    root metadata; favicon.ico was already referenced.
  • Live cursors in the card description (ADR 0011, part 2 of 2) —
    with NEXT_PUBLIC_FEATURE_LIVE_CURSORS="true", the description field
    becomes a CRDT-aware CodeMirror 6 editor (y-codemirror.next)
    bound to the same card Y.Text, rendering remote collaborators'
    carets + selections in their presence colour. Markdown-source model
    unchanged — Save / export / encryption paths are untouched; the
    editor is dynamically imported (no SSR) so its bundle never hits the
    initial page load, and it falls back to the plain textarea when the
    flag is off (default). Consumes the encrypted cursor channel from
    part 1. New deps: @codemirror/{state,view,commands,lang-markdown} +
    y-codemirror.next. Live cursors are now feature-complete behind
    the flag
    (requires a WS relay redeploy from this version).
  • Live-cursor transport (ADR 0011, part 1 of 2) — groundwork for
    Google-Docs-style cursors in the card description. The WS relay now
    forwards a new awareness message type on the existing card:<id>
    room (additive — pre-cursor clients never send it, and it's relayed
    as opaque ciphertext like document updates). New client
    CardCursorProvider (src/lib/realtime/card-cursors.ts) hosts a Yjs
    Awareness over the card's actual Y.Doc, encrypting each frame
    under the BoardKey bound to card:<id>:cursor. It leaves the working
    CardWsProvider doc-sync path untouched (separate message type,
    ignores update frames). ⚠️ Crypto-relevant. The awareness payload
    is PII-free ({ userId, color } only — guarded by a unit test); the
    CodeMirror editor that consumes this lands in part 2, behind
    NEXT_PUBLIC_FEATURE_LIVE_CURSORS (default off). Operators must
    redeploy the WS service from this version before the feature can be
    switched on.

Changed

  • Release images for pre-release (beta) tags are now linux/amd64
    only
    (#72). Stable tags still publish multi-arch amd64 + arm64;
    dropping the emulated arm64 build for betas roughly halves release-CI
    time. Pull a stable tag if you need an arm64 image.

Fixed

  • 2FA enrolment dead-end — after enabling two-factor auth the user
    was stranded on the confirmation screen with no way forward. It now
    auto-redirects to /workspaces after a short pause and shows a
    "Continue to your workspaces" link as a manual fallback (works even
    if the redirect is blocked).
  • Apache CSP example clobbered Next.js's nonce CSP (apache/mskanban.conf.example)
    — the vHost set Content-Security-Policy itself, which overrides the
    per-request, nonce-based CSP that src/proxy.ts emits for inline
    hydration scripts. Result on a real deploy: every inline hydration
    <script> is blocked, the page never hydrates, and the whole app is
    non-interactive (login/register/boards silently dead, no server-side
    log because the request never reaches Node). setifempty doesn't fix
    it either — behind mod_proxy it adds a second, nonce-less CSP and
    the browser enforces the intersection. The example now sets no CSP
    in Apache (Next.js owns it; every HTML route is covered by the
    proxy.ts matcher) and documents the trap, plus a troubleshooting
    entry in docs/deployment/auto-deploy.md. Discovered debugging a
    production registration outage.

What's Changed

  • ci(release): build arm64 only for stable tags by @Musiker15 in #72
  • feat(realtime): live-cursor transport (ADR 0011, part 1/2) by @Musiker15 in #73
  • feat(editor): CodeMirror live-cursor description editor (ADR 0011, 2/2) by @Musiker15 in #74
  • fix(apache): don't set CSP in the vHost — Next.js owns the nonce CSP by @Musiker15 in #75
  • fix(ui): 2FA redirect dead-end + brand logo/favicon/PWA icons by @Musiker15 in #76
  • Release v0.4.0-beta by @Musiker15 in #77

Full Changelog: v0.3.0-beta...v0.4.0-beta

v0.3.0-beta

28 May 16:16
7038044

Choose a tag to compare

v0.3.0-beta Pre-release
Pre-release

Completes the Automation Engine (all five triggers + four actions, end-to-end zero-knowledge), ships a production hot-fix for the WebSocket relay that had been returning 503 since the first auto-deploy, and clears the remaining v0.2.0-beta clean-up backlog (Column.isDone, Card.startAt export/import round-trip). Also lands ADR 0011 (proposed) for live cursors in the description editor — implementation gated on its acceptance.

Added

  • Automation trigger card_due_reached — the last v1 trigger from
    ADR 0010, completing the automation engine. A new in-process
    per-minute scheduler (src/lib/automation/due-scheduler.ts, started
    from instrumentation.ts like the webhook worker — no separate
    process or systemd unit) scans for cards whose dueAt passed within
    a 7-day look-back on boards that have an active due-rule, claims each
    card once via Redis SET NX (30-day TTL), and publishes a
    content-free AUTOMATION_DUE_FIRED board tick. The next online
    client builds the card_due_reached event from its current card
    state and runs the rule through the normal evaluator/executor —
    the server never decrypts enc_rule, it only reads the already-plain
    dueAt. Due rules can be scoped by column or milestone. Disable the
    scheduler with AUTOMATION_DUE_WORKER_DISABLED=1. New evaluator +
    eventToken tests (43 automation cases total).
  • Automation action post_comment — rules can now post a comment as
    a side-effect (e.g. "When moved to Done → post Closed on {{date}}").
    The body is a template ({{date}} / {{datetime}} tokens, expanded
    client-side with split/join — no regex/ReDoS) that lives encrypted
    in enc_rule; the executor encrypts the rendered comment under the
    BoardKey (bound to comment:new) so it's E2EE exactly like a human
    comment. Because it's observable + non-idempotent, it ships with the
    Redis SETNX idempotency claim from ADR 0010: the comment POST accepts
    an optional idempotencyKey (<ruleId>:<eventToken>), the server
    SETNX automation:claim:<key> with a 60 s TTL, and a lost race returns
    { deduped: true } instead of a duplicate. Redis hiccup → fail open
    (post the comment). New rule-builder UI: action picker + comment
    textarea. ⚠️ Crypto-relevant — see PR. New tests: executor
    expandTemplate (5), DSL post_comment validation (3), evaluator
    eventToken + ruleId threading.
  • Automation triggers card_label_added + comment_added are now
    live
    — both existed in the DSL since v1 but their client emitters
    were deferred. The card drawer now fires card_label_added when a
    label is attached (not on removal) and comment_added after a
    comment is successfully posted; both route through the existing
    emitAutomationTriggerevaluateAllexecuteAll path. The rule
    builder gained an "Only when this label is added" condition selector
    for card_label_added (scoped to a labelId). No-cascade invariant
    documented: automation actions hit the REST API directly and never
    re-enter the emitting path, so set_label can't loop back into a
    card_label_added rule (ADR 0010 §Risks). New evaluator tests
    (13 cases total, +3).
  • Explicit "Done" columns (Column.isDone) — columns can now be
    pinned as "done" from a ✓ toggle in the column header (with a DONE
    badge), replacing the analytics "last column by position = done"
    heuristic. Any number of columns can be flagged. New boards seed the
    default "Done" column with the flag automatically. Plain
    server-visible boolean (like position/wipLimit) — no E2EE impact.
    Boards created before the flag existed fall back to the legacy
    heuristic, so nothing breaks without a backfill. New migration
    20260528000000_column_is_done, pure selectDoneColumnIds helper in
    src/lib/analytics/done-columns.ts (5 unit tests).
  • Card start date in foreign importers — the Trello importer now
    maps a card's start field, and the CSV importer recognises a
    start / startAt / start date column, both feeding the native
    startAt. Brings imported boards in line with the Timeline/Gantt
    view, which keys range bars off startAt.
  • docs/deployment/mskanban-ws.service — production systemd unit
    for the WebSocket relay (Yjs sync + board presence). Mirrors the
    hardening profile of mskanban.service: User=mskanban,
    EnvironmentFile=/opt/mskanban/.env, the full
    Protect*/Restrict*/SystemCallFilter set, IPAddressDeny=any
    with localhost-only allow, zero CapabilityBoundingSet,
    IPAddressAllow=localhost. ExecStart=tsx src/server/ws/index.ts
    listens on WS_PORT (default 3001), matching the existing Apache
    mod_proxy_wstunnel rule for /api/ws. deploy.sh was already
    ready for it — step 8 restarts mskanban-ws.service if and only
    if
    the unit is installed, so existing deployments roll forward by
    copying the file + systemctl enable --now mskanban-ws.

Fixed

  • Card.startAt lost on board export/import — the native JSON
    export omitted startAt entirely and the importer never restored it,
    so a board round-trip silently dropped every card's start date (and
    with it the Gantt range bars). ExportedCard now carries startAt,
    exportBoard populates it, the Markdown export renders a
    start → due range (or a start-only / due-only marker), and the
    importer PATCHes startAt alongside dueAt + archived. Pre-#49
    snapshots without the field import cleanly (startAt ?? null). New
    unit tests: board-export (4 cases) + foreign-import (3 cases).
  • Production WS-relay outage/api/ws returned 503 since the
    first v0.2.0-beta auto-deploy because no process listened on
    127.0.0.1:3001. Effect: cross-user Yjs sync + board presence (the
    "real-time collaboration" USP) were dead; single-user editing kept
    working because Yjs writes also go through the HTTP PATCH path.
    Root cause: v0.2.0-beta shipped the deploy framework + WS-restart
    hook but not the systemd unit itself — listed as a deferred item.
    Now both are in the tree; installing the unit on the server is a
    one-liner (see docs/deployment/auto-deploy.md §4).
  • Docs drift in docs/deployment/auto-deploy.md — the "WS unit is
    on the to-do list" footnote in §4 is replaced by the actual
    install + verify recipe, including an ss -ltnp port check and a
    curl upgrade-handshake probe that distinguishes the relay being
    down (503) from a normal "missing ticket" reject (400).

What's Changed

  • chore(deps): bump docker/setup-buildx-action from 3 to 4 by @dependabot[bot] in #58
  • chore(deps): bump actions/attest-build-provenance from 1 to 4 by @dependabot[bot] in #59
  • chore(deps): bump webfactory/ssh-agent from 0.9.0 to 0.10.0 by @dependabot[bot] in #60
  • chore(deps): bump docker/metadata-action from 5 to 6 by @dependabot[bot] in #61
  • fix(ci): use default token for sponsors workflow checkout by @Musiker15 in #62
  • Fix/update sponsors workflow by @Musiker15 in #63
  • fix(deploy): ship mskanban-ws.service systemd unit by @Musiker15 in #64
  • fix(export): round-trip Card.startAt through export/import by @Musiker15 in #65
  • feat(analytics): explicit Column.isDone flag by @Musiker15 in #66
  • feat(automation): wire card_label_added + comment_added triggers by @Musiker15 in #67
  • feat(automation): post_comment action + Redis SETNX idempotency by @Musiker15 in #68
  • feat(automation): card_due_reached trigger + in-process scheduler by @Musiker15 in #69
  • docs(adr): 0011 live cursors in description editor (proposed) by @Musiker15 in #70
  • docs(changelog): cut v0.3.0-beta by @Musiker15 in #71

Full Changelog: v0.2.0-beta...v0.3.0-beta

v0.2.0-beta

25 May 17:34
24d7fd5

Choose a tag to compare

v0.2.0-beta Pre-release
Pre-release

The first sweep of post-beta work. Highlights: Milestones + Burn-Down + Timeline (Gantt) for time-/scope-based planning; board-level presence via Yjs awareness over the existing E2EE WS relay; Automation Engine v1 (declarative {when, do} rules, zero-knowledge, ADR 0010); MSKanban docs site under docu.msk-scripts.de/ecosystem/mskanban; auto-deploy from GitHub Actions to the production server with a ForceCommand-locked SSH key.

Added

  • Milestones (#39 + #40 + #41 + #48) — group cards by deliverable with an optional date window. Server sees the date range + board association (needed for filtering / charts); name + description live encrypted under the BoardKey. New Milestone model, REST routes, board-level manager modal, card-drawer selector, per-card badge, and a Burn-Down chart per milestone in the Analytics view.
  • Timeline / Gantt view (#49) — fifth board tab next to Board / Calendar / Table / Analytics. Cards with startAt + dueAt render as range bars; only-dueAt cards as diamonds; missing-data cards are counted in the footer hint. Day / Week / Month zoom, vertical "today" line, arrow-key pan + +/- zoom (WCAG keyboard alternative). Pure SVG renderer.
  • Card startAt editor (#49) — the field has existed in the Card model since Phase 3 but was never UI-editable. card-drawer.tsx now exposes a start-date input alongside the existing due-date input; the Gantt bars depend on it.
  • Board-level presence (#50) — Yjs Awareness over the existing E2EE WebSocket relay. Avatar-stack of online users in the board header (max 5 + "+N"); per-card coloured dots when other users have the card drawer open. Awareness payload is {userId, color, viewing?} only — no email, no titles, no description text. Server only sees opaque ciphertext bound to board:<id>:awareness. Colour is a deterministic HSL hash of userId. New BoardPresenceProvider (src/lib/realtime/awareness.ts), useBoardPresence React hook, and a new room kind board:<id> on the relay with workspace-member authorisation.
  • Automation Engine v1 (#52, implements ADR 0010) — declarative {when, do} rules per board, fully zero-knowledge. Rule bodies live encrypted in enc_rule; a small plaintext trigger envelope (trigger_type + trigger_meta) is mirrored in two new columns so a future BullMQ scheduler can route ticks for time-based triggers without breaking E2EE. The envelope is validated against a strict whitelist on every write so the metadata field cannot become a back channel for plaintext leakage. v1 wired triggers: card_created, card_moved; v1 actions: set_label, assign_member, move_to_column (all idempotent at the repo layer — Redis SETNX claim deferred together with post_comment).
  • MSKanban documentation site — published as the third ecosystem entry on the existing Docusaurus instance (docu.msk-scripts.de/ecosystem/mskanban). Seven pages: overview, installation (Docker + bare-metal Apache), getting-started, features, REST API, privacy & security (full key hierarchy + threat model), FAQ.
  • Auto-deploy from GitHub Actions to the production server (#54, #55, #56) — workflow_run triggers on CI success, SSHes via a ForceCommand-locked action key, runs scripts/deploy.sh on the server (self-updating, idempotent, hard-fail on build / migration / health-check errors, optional WS-relay restart, deploy tags for rollback). Strict known_hosts check via DEPLOY_HOST_FINGERPRINT secret; no StrictHostKeyChecking=no anywhere. Full setup walkthrough in docs/deployment/auto-deploy.md.
  • ADRs0010 Automation Engine added; previous ADRs index updated.
  • New unit-test suitesautomation-dsl (14 cases, #52), automation-evaluator (10 cases, #52), user-color (6 cases, #50), totp (17 cases, #35). 137 tests total at release time.

Changed

  • KanbanCard view-model gains startAt: string \| null (#49) — propagated through all five construction sites in board-client.tsx (initial load, SSE refetch, create-from-scratch, create-from-template, drawer reconcile). The previously-hardcoded startAt: null in the <CardDrawer> wiring is now openCard.startAt.
  • Repo-URL cleanup (#53) — every github.com/musiker15/mskanban and ghcr.io/musiker15/mskanban reference in README.md, CHANGELOG.md, SECURITY.md, CONTRIBUTING.md, CLAUDE.md, docs/public-launch.md, docs/deployment/mskanban.service rewritten to the canonical MSK-Scripts/mskanban. The release.yml workflow already pushed containers to ghcr.io/${{ github.repository_owner }} (= MSK-Scripts) so the broken README badges were the only visible symptom. Also normalised the Code-of-Conduct contact e-mail to @msk-scripts.de.
  • README post-beta refresh (#53) — "five views per board" (Timeline added), "WebAuthn / Passkeys planned" → both shipped, Milestones / Burn-Down / Timeline / Presence / Automation added to the feature highlights, broken docs/crypto/ link replaced with ADR 0003 + threat-model, link to the new docs site.
  • 21 Dependabot bumps consolidated into the v0.1.0-beta → v0.2.0-beta window. First sweep (low-risk, single 2026-05-25 batch): @types/node 22→25, prettier-plugin-tailwindcss 0.6→0.8, eslint-config-prettier 9→10, vitest 3→4, @hookform/resolvers 3→5, @dnd-kit/sortable 9→10, jsdom 25→29. Second sweep (afternoon): 14 PRs across 5 GitHub Actions majors (docker/setup-qemu-action 3→4, actions/cache 4→5, actions/download-artifact 4→8, github/codeql-action 3→4, actions/upload-artifact 4→7) and 9 npm bumps (bullmq 5.77.2→5.77.3, lint-staged 15→17, pino-pretty 11→13, typescript 5.9→6.0, tailwind-merge 2→3, pino 9→10, lucide-react 0.460→1.16, zod 3→4, y-websocket 2→3). All shipped CI-green incl. Playwright E2E.
  • @vitest/coverage-v8 3 → 4 — peer-match for the vitest 4 bump.

Removed

  • Phase-3 plaintext plumbingsrc/lib/encoding/plaintext-blob.ts, tests/unit/plaintext-blob.test.ts, and scripts/migrate-phase3-to-e2ee.ts are gone. ADR 0007 is "executed": every data-bearing API and every read path now goes through the Phase-4 v1.<nonce>.<ct> envelope (28 call sites across workspace, board, card and card-drawer clients). The encodeBlob / decodeBlob helpers had zero consumers left.
  • @otplib/core removed as a direct dependency — was unused in src/. Stays in the tree as a transitive of otplib, so no functional change.

Fixed

  • Flaky tests/unit/totp.test.ts clock-drift cases — original wall-clock arithmetic (Math.floor(Date.now() / 1000) - 31) jumped two 30-s steps back instead of one when Date.now()/1000 mod 30 was 0 – 1, flaking ~3 % of CI runs. Re-implemented with vi.useFakeTimers() locked to Unix second 1000 (mid-step, well above otplib v13's non-negative-epoch guardrail). 10× local stress loop now passes cleanly. Hit Dependabot PRs #32 and #33 before the fix.
  • Inline # comment on env values in .env.example (#56) — ATTACHMENT_MAX_BYTES="26214400" # 25 MiB and LOG_LEVEL="info" # ... were copy-pasted into production .env files where systemd EnvironmentFile= does not strip trailing comments, breaking the app's zod env-schema at startup (Number("26214400 # 25 MB") = NaN). Both moved to comment-above-value. Discovered live during the first production auto-deploy.

Security

  • otplib 12 → 13 — auth-critical API rewrite. The authenticator namespace is gone; v13 ships top-level functional exports with the default verify now async. src/lib/auth/totp.ts deliberately uses verifySync instead of the async verify — in this path a missed await would always evaluate the returned Promise as truthy and silently bypass 2FA. epochTolerance: 30 s reproduces the old window: 1. Existing User.totpSecret rows remain valid; v13's new 16-byte minimum-secret guardrail does not reject the 20-byte secrets our generateSecret() produces.
  • @simplewebauthn/{browser,server} 11 → 13 — kept in lock-step with the otplib bump even though @simplewebauthn is not yet used in src/ (reserved for the upcoming WebAuthn phase). Avoids future split-version surprises.

Tooling

  • .github/dependabot.yml — the crypto group (libsodium*, argon2-browser, @simplewebauthn/*, otplib) is now restricted to update-types: [minor, patch]. Major bumps in any auth-critical package now arrive as individual PRs so they can be reviewed against src/lib/auth/* in isolation.
  • .gitattributes*.sh + *.service forced to text eol=lf. Without this, Windows-side git's default LF→CRLF on checkout would silently break the shebang of scripts/deploy.sh on the Linux server with a cryptic ^M: command not found.

Deferred (not landed in this sweep)

  • eslint 9 → 10 — blocked on upstream eslint-plugin-react (pulled in via eslint-config-next) which still uses the ESLint 9 context API removed in 10.
  • @vitejs/plugin-react 5 → 6 — needs Vite 8 as a direct dependency; we only have Vite 6 transitively today.
  • Automation triggers card_label_added + comment_added — exist in the DSL but their emitters need card-drawer wiring; separate PR.
  • Automation post_comment action + card_due_reached trigger — both need the Redis SETNX idempotency claim (post_comment) and the per-minute BullMQ scheduler (card_due_reached) from ADR 0010 §"Conflict resolution".
  • Live cursors in the card description editor — needs swapping the plain <textarea> for a CRDT-backed editor (TipTap / CodeMirror) that exposes cursor positions as observable state. Separate change, separate ADR.
  • board-export.ts round-tripping startAt — the field is now edita...
Read more

v0.1.0-beta

24 May 22:59

Choose a tag to compare