Skip to content

wekan

v9.35 Breaking

This release includes breaking changes for platform teams planning a safe upgrade.

Published 1mo Productivity & Wikis
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

docker javascript kanban meteor real-time sandstorm
+2 more
snapcraft wekan

Affected surfaces

auth rbac

Summary

AI summary

Broad release touches https://github.com/wekan/wekan/blob/main/docs/hall-of-fame/clonebleed/, https://github.com/wekan/wekan/commit/a86a087915aac9d204e157b1a698091c079e04e6, https://github.com/wekan/wekan/commit/dca9427bfb58532778bc705e4c8c8014ba4b01ae, and https://github.com/wekan/wekan/commit/357de728c03113b787065bac2c5832ad77f1a117.

Full changelog

TLDR:

  • Admin Panel/Attachments/Move Files works: CollectionFS/Meteor-Files/S3/Azure Blob/Google Cloud Storage.
  • To have attachments and avatars visible, move them from CollectionFS to any other Storage
  • To upgrade, mongodump/mongorestore (older with LD_LIBRARY_PATH)
    to MongoDB 7.x, and copy WRITABLE_PATH files/attachments/avatars if exists,
    at Snap /var/snap/wekan/common/files/, at Docker /data/files etc.

This release adds the following CRITICAL SECURITY FIXES:

  • Fix GHSA-qfqv-42qw-vvwh: cloneBoard Meteor method has no authorization check — any user can clone (read) any private board by ID (CWE-639, CWE-862).
    The cloneBoard Meteor method in models/import.js copied an entire board —
    including all cards, comments, attachments, member info and activities —
    identified solely by a caller-supplied sourceBoardId, and performed no
    authorization check: it never verified that the calling user was a member of
    (or otherwise permitted to read) the source board. Any authenticated Wekan
    user who knew a board's ID (board IDs appear in board URLs and remain known to
    removed members) could call Meteor.call('cloneBoard', '<targetBoardId>')
    over DDP and obtain a permanent, fully-readable copy of that board's contents,
    even for private boards they had no access to. The method called
    exporter.build() directly, skipping the canExport() guard
    (models/exporter.js) that the REST export route correctly enforces. Fixed by
    requiring this.userId and running the same exporter.canExport(user) check
    the export route uses before building/cloning the board, so cloning a board
    now requires the same read authorization as exporting it.
    CVSS 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N).
    Thanks to dizconnectz for the coordinated disclosure, xet7 and Claude.

and, while fixing the above, the following similar authorization issues found by code review were fixed (the CloneBleed group):

  • Fix authorization guards that silently never ran in attachment, list, checklist, migration and webhook server methods (CWE-862, CWE-639).
    A review for the same class of bug as cloneBoard found several server-side
    methods whose access checks were present in the source but never actually
    enforced, plus a few methods missing a check entirely:
    • server/models/checklists.js moveChecklist: the membership guard called
      allowIsBoardMemberByCard(...), but that helper is async and was both
      unimported and un-awaited, so !allowIsBoardMemberByCard(...) evaluated
      !Promise (always false) and the check never ran — any logged-in user
      could move a checklist between cards on boards they cannot access. Now
      imported and awaited, and login is required.
    • server/models/lists.js updateListSort: the guard read
      typeof allowIsBoardMember === 'function' && !allowIsBoardMember(...), but
      allowIsBoardMember was never imported, so the typeof was always false
      and the guard never ran — any caller could reorder/re-assign any board's
      lists by ID. Replaced with a real hasBoardWriteAccess check, plus a check
      that the list actually belongs to the named board, and a login check.
    • server/attachmentApi.js and server/routes/attachmentApi.js: the
      attachment Meteor methods and REST handlers called board.isBoardMember(...),
      which is not a method on the board model, so the permission check never
      behaved as intended. Replaced with the real board.hasMember(...), and the
      upload path now also verifies the target card actually belongs to the named
      board (so boardId cannot be spoofed to a board the caller is a member of).
    • server/models/users.js applyListWidth: stored per-board list width with
      no authorization. Now requires the caller to be a member of the board and
      requires the list to belong to that board.
    • server/models/boards.js getBackgroundImageURL: returned any board's
      background image URL by ID. Now requires the board to be visible to the
      caller.
    • server/migrations/fixMissingListsMigration.js and
      server/migrations/migrateAttachments.js: the migration status/execute
      methods only checked that the caller was logged in. Reading status now
      requires board visibility, executing a board-rewriting migration now requires
      board admin (or instance admin), and the attachment migration methods now
      require board access (global status reads require instance admin).
    • server/notifications/outgoing.js: the webhook delivery method trusted the
      caller-supplied integration object and only checked that some integration
      with that URL existed. It now verifies a matching integration exists on its
      own board and that the caller is a member of that board, closing a path where
      any authenticated user could drive webhooks (and, via the two-way response
      path, overwrite comments) on boards they cannot access.
    • server/models/userPositionHistory.js: the
      userPositionHistory.createCheckpoint, .getRecent, .getCheckpoints and
      .restoreToCheckpoint methods only checked that the caller was logged in,
      even though the sibling positionHistory.track* methods
      (server/methods/positionHistory.js) already require the board to be visible
      to the caller — the same PositionHistoryBleed class fixed in v8.20/v8.21.
      All four now require board visibility (via the shared isVisibleBy guard)
      before reading or restoring position history scoped to a board.
      Thanks to xet7 and Claude.

and, while auditing that client-side permission checks are also enforced server-side, the following gaps where the server did not re-verify a UI-gated permission were fixed:

  • Fix client-side permission gates not re-verified server-side (CWE-862, CWE-269).
    The browser UI hides certain actions from read-only members and from
    non-admins, but those are cosmetic — the server-side allow rules and Meteor
    methods must enforce the same role. An audit found four places that did not:
    • server/permissions/customFields.js: the Custom Field allow
      insert/update/remove rules used allowIsAnyBoardMember (mere membership), so
      read-only/comment-only/worker members could create/modify/delete Custom
      Fields (board-wide schema) via direct DDP collection writes — the same
      read-only-write class as GHSA-6733, which had only fixed the REST path. Now
      uses the new allowIsAnyBoardMemberWithWriteAccess write-access helper.
    • server/permissions/cardCommentReactions.js: the reaction allow rules used
      allowIsBoardMember, letting read-only members add/remove comment reactions.
      Reacting is a form of commenting, so it now uses allowIsBoardMemberCommentOnly
      (Normal/Comment-only allowed, Read-only/No-comments denied), matching
      CardComments.insert.
    • server/models/boards.js archiveBoard: only required board membership, so
      any member (including read-only) could archive a board (hiding it for
      everyone) over DDP, although the UI gates archiving behind board admin. Now
      requires board admin (or global admin), matching the Boards.allow
      update/remove rules.
    • server/models/settings.js sendSMTPTestEmail: only required login, so any
      authenticated user could trigger the server to send an SMTP test (and have
      the server's SMTP error messages surfaced to them), although the UI gates it
      behind global admin. Now requires global admin.
      Thanks to xet7 and Claude.

and, in the same access-control audit, the attachment write API was tightened:

  • Security: attachment write operations in the REST/DDP API now require board
    write access, not just membership (CWE-862, CWE-639).
    Both attachment API
    implementations (server/routes/attachmentApi.js HTTP routes and
    server/attachmentApi.js Meteor methods) gated upload / copy / move / delete
    on board.hasMember(), which is true for any active member regardless of role.
    This let read-only, comment-only, no-comments, worker and assigned-only board
    members add, copy, move and delete attachments through the API — actions the UI
    forbids for those roles. They now require board write access (global site
    admins still allowed); read operations (download / list / info) keep
    membership-level access. Added security tests in
    server/lib/tests/attachmentApi.tests.js.
    Thanks to xet7 and Claude.

and adds the following new features:

  • Admin Panel / Attachments:
    "Calculate file counts" on every storage backend,
    shown right below the "Read" toggle.
    Azure Blob Storage and Google Cloud
    Storage now have a "Calculate file counts" button (new getAzureStorageStats /
    getGcsStorageStats methods) that reports how many attachments and avatars are
    stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already
    offer. The count is read from the stored file metadata (fast, no cloud API
    calls). The S3 button was also moved up to sit directly under its "Read"
    checkbox, so all backends are consistent. Thanks to Claude.
  • **Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy files visible.
    ** A translatable line under the "MongoDB GridFS Storage" title
    reminds admins: "To have attachments and avatars visible, move them from
    CollectionFS to any other Storage." Thanks to Claude.
  • Admin Panel / Attachments: the Google Cloud Storage page now documents the
    required bucket permission.
    A note under the "GCS Storage" title explains that
    the service account needs the Storage Object Admin role
    (roles/storage.objectAdmin) on the bucket (Cloud Console → Cloud Storage →
    Buckets → your bucket → Permissions → Grant access → add the service account →
    role "Storage Object Admin"), and that the bucket must exist in the same
    project — otherwise "Test connection" fails with storage.objects.list denied.
    Thanks to xet7 and Claude.
  • Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles
    at the top of each cloud storage.
    S3, Azure and Google Cloud Storage now have
    a Save button directly below the "[ ] Enabled [ ] Read" row, so those toggles
    can be saved without scrolling to the bottom of the (now quite long) storage
    page. It saves the same way as the existing bottom Save button — the server
    merges that provider's config and preserves secrets left blank — and the bottom
    Save button is kept. (Filesystem and GridFS have only a Read toggle, which
    already saves the instant it is toggled, so they need no button.) Thanks to Claude.
  • Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields
    (S3, Azure and Google Cloud Storage).
    Each cloud-storage setting now guides
    the admin from top to bottom with both English and translated text, so it is
    easy to follow along while clicking through the provider's Cloud Console:
    • the field label uses the provider's own console wording in English
      (e.g. Access key ID, Secret access key, Storage account key,
      Service account key (JSON)), with the translated label shown below it;
    • a literal example of what the value should look like
      (e.g. eu-west-1, an example bucket name / endpoint / connection string),
      which is intentionally not translated;
    • a short description of the value, in English and then translated;
    • the Cloud Console menu path showing exactly where to find or create the
      value (e.g. AWS Console → IAM → Users → your user → Security credentials →
      Access keys → Create access key
      ; Azure Portal → Storage accounts → your
      account → Access keys → key1
      ; Google Cloud Console → IAM & Admin →
      Service accounts → … → Keys → Add key → Create new key → JSON
      ), again in
      English and then translated.
      The provider field names, examples and menu paths are kept in English so they
      match what the Cloud Console actually shows, while the descriptions and menu
      paths are also translatable; section titles, the enable/read/force-path-style
      checkboxes and the Test/Save buttons remain fully translated. Thanks to Claude.
  • Admin Panel / Attachments / Move Attachment: "Repair file locations" button
    and a persistent "last move" message.
    The new Repair file locations button
    scans all attachments and avatars and finds any whose recorded storage
    (versions.<v>.storage / path / meta.gridFsFileId) no longer matches where
    the binary actually is — left inconsistent by an interrupted or failed move —
    and fixes the database to point at the real location: it detects the binary in
    GridFS (by the metadata.fileId stamped on upload, recovering files whose
    GridFS id reference was lost) or on the filesystem (using the storage
    strategy's own thorough path resolution, so the repair agrees with what a real
    download/move would find even when versions.<v>.path is stale), then corrects
    storage, path and meta.gridFsFileId accordingly. Cloud-stored files are
    left untouched, and files found nowhere are reported as "Not found". The scan is
    streamed (memory-safe) and shows a per-scope searched / repaired / not-found
    summary. Separately, after a move finishes the page now keeps showing
    the last move operation — source → destination (scope) and the date/time as
    YYYY-MM-DD HH:MM:SS — so it is clear what was last done.
    Thanks to xet7 and Claude.
  • SVG image uploads are now sanitized instead of rejected. Uploaded SVGs
    (attachments and avatars) are cleaned in place in onAfterUpload via the new
    models/lib/sanitizeSvg.js, which removes JavaScript (<script>, inline
    on*= event handlers, javascript:/vbscript: URIs, <foreignObject> /
    <iframe> / <object> / <embed> and similar active content) and XML loops
    (<!DOCTYPE> / <!ENTITY> entity-expansion / XXE constructs and
    <?xml-stylesheet?>), so SVG images can be uploaded safely. Thanks to Claude.
  • Unified attachment/avatar storage migration: move any → any. Admin Panel /
    Attachments / Move Attachment can now move Attachments, Avatars, or both,
    from any source to any destination across Filesystem, Meteor-Files GridFS,
    Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is
    "All Read-enabled storages" (every backend whose Read flag is enabled and whose
    settings work), so everything can be consolidated into one destination in a
    single run. All metadata is preserved (board / swimlane / list / card / user /
    uploaded date / name / type / size), attachment cover references
    (cards.coverId) are remapped when an id changes, and the legacy source is
    deleted only after the new copy is verified. Thanks to Claude.
  • Legacy CollectionFS GridFS is now a first-class storage backend (read,
    migrate-from, and export-to) for both attachments and avatars, via the new
    models/lib/collectionFsStore.js (binary keyed by copies.<coll>.key in the
    cfs_gridfs.<coll> bucket, metadata in cfs.<coll>.filerecord). Thanks to Claude.
  • The storage strategy layer is now collection-aware. moveToStorage and the
    GridFS/Cloud strategies previously hard-coded the Attachments collection, so
    moving avatars to GridFS/cloud would have updated the wrong collection. The
    factory now carries its collection (Attachments or Avatars), so avatars
    can be stored in Meteor-Files GridFS and cloud
    , not only on the filesystem.
    Attachment behavior is unchanged.
    Thanks to xet7 and Claude.

and adds the following updates:

  • Update Windows MongoDB.
    Thanks to xet7.
  • Update DDP transport and reactivity order for multi-tenant deployment.
    Thanks to italojs.
  • Updated to Meteor 3.5-rc.1.
    Thanks to Meteor developers.
  • Documented the attachment / file REST API in the OpenAPI docs — file
    upload, download, info, listing a board's files, copy, move and delete (the
    /api/attachment/* endpoints registered via WebApp.handlers.use(), which the
    generator cannot auto-discover) are now documented in openapi/extra_paths.yml
    and injected into the generated docs, including their authentication and
    permission requirements.
    Thanks to xet7 and Claude.

and adds the following fixes:

  • Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the database.
    MongoDB refuses compact on an active replica-set primary unless
    force: true is given, so every collection failed with "will not run compact
    on an active replica set primary … use force:true to force" — and because the
    usual Meteor setup is a single-node replica set (only a primary, no
    secondaries), nothing was compacted at all and no disk space was reclaimed. The
    primary is now compacted with force: true (secondaries are still compacted
    without it, so the primary stays available while they run), so the GridFS
    collections are actually rewritten and freed space is returned to the
    filesystem. Thanks to Claude.
  • Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud
    config inputs are trimmed.
    Testing an Azure/GCS/S3 connection used to report a
    single generic Incomplete configuration or adapter not installed even when the
    real problem was specific — e.g. Azure rejecting the config with Invalid URL
    (a stray space/newline in the account name, or a bad endpoint / connection
    string). testCloudConnection now reports the actual cause: adapter not
    installed, required fields missing, the adapter's own configuration error (such
    as Invalid URL), or the real listFiles error (auth failure, container not
    found, …). It also pre-validates the config and gives an actionable message
    before the adapter turns it into a cryptic error — e.g. for Azure it explains
    that the Storage account name must be just the account name (3–24 lowercase
    letters/numbers, e.g. wekanstorage, not a full https://… URL and with no
    spaces), or that the Connection string is malformed; and when Azure still
    returns Invalid URL the message appends which field to check. In addition, all
    cloud-storage text fields are now trimmed when read from the form, so
    leading/trailing whitespace from copy-paste no longer produces an invalid URL.
    Thanks to xet7 and Claude.
  • Fixed moving attachments to S3
    (and other cloud storage) failing and crashing the server.

    Uploading to S3 failed two ways, each of which crashed
    the whole server because the rejection was unhandled and SyncedCron treats
    those as fatal. The @tweedegolf/sab-adapter-amazon-s3 adapter uploaded with a
    PutObjectCommand whose Body was a live stream: (1) with no ContentLength
    the AWS SDK fell back to a chunked signed upload that needs a decoded content
    length, which was undefined for a stream of unknown size — Invalid value "undefined" for header "x-amz-decoded-content-length"; and (2) if the socket
    dropped mid-upload (socket hang up), the SDK's body-stream promise rejected
    unhandled. Fixed by uploading the file as a complete in-memory buffer
    (addFileFromBuffer) instead of a live stream: a buffer has a known length (no
    content-length error) and no socket-bound stream to re-reject. The cloud upload
    promise now never rejects — any failure is captured and exposed via
    waitUntilStored(), which moveToStorage checks inside a try/catch so a failed
    upload leaves the source file intact and is logged cleanly. (Files are moved one
    at a time, so peak memory is one file.) Thanks to Claude.
  • Fixed TypeError: Cannot read properties of undefined (reading 'on') when
    moving attachments between Meteor-Files/GridFS and Filesystem.
    If a file's
    source binary was missing at its recorded location (e.g. a file left
    half-moved by an earlier interrupted run — storage says "gridfs" but the
    GridFS id reference is gone, or a filesystem path that no longer exists),
    getReadStream()/getWriteStream() returned undefined and moveToStorage
    (and copyFile) called .on() on it, throwing. Both now detect a missing
    read/write stream, log a clear message (which file, version, from→to storage,
    and why), skip that file and leave the source intact so no data is lost,
    and continue with the rest. Use the new "Repair file locations" button to fix
    the underlying inconsistent records. Thanks to Claude.
  • Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing
    the server, hanging the Admin Panel move at "File 1 / N", and leaving broken
    avatars.
    Three problems in Admin Panel / Attachments / Move Attachment:
    • Server crash on the first file (uncaughtException: TypeError: Cannot read properties of undefined (reading '_id') at
      AttachmentStoreStrategyGridFs.writeStreamFinished). The current mongodb
      driver's GridFS GridFSBucketWriteStream 'finish' event no longer passes
      the stored file document, so finishedData._id threw and killed the process
      (which is also why the move appeared stuck at "File 1 / 4" — the background
      job died mid-file and its persisted status was frozen). The GridFS strategy
      now reads the uploaded file id from the write stream itself (gridFSFile._id
      / id), the 'finish' handler is wrapped so it can never crash the process,
      and a startup reconciliation clears a stale "running" move status left by a
      crashed run so the UI un-sticks and a new move can be started.
    • Broken avatar after migrating avatars. The bulk move only repointed card
      cover references (cards.coverId); a user's profile.avatarUrl still pointed
      at the deleted legacy /cfs/files/avatars/<oldId> URL, so the avatar rendered
      broken. The move now repoints profile.avatarUrl to the migrated avatar
      (remapReferences handles avatars), and migrated files stamp
      meta.migratedFromId so references can be repaired to the exact new file.
      The startup repair in server/models/users.js (previously a no-op: it
      string-replaced the URL prefix while keeping the now-deleted id and never
      saved the document) now points each affected user's avatar at their migrated
      Meteor-Files avatar — matched precisely by meta.migratedFromId, otherwise
      strictly by userId (newest migrated avatar first), so a user can only ever
      be given their own avatar, never another user's.
    • ObjectID deprecation warning from models/lib/grid/createObjectId.js
      (MongoInternals.NpmModule.ObjectIDObjectId).
      Thanks to xet7 and Claude.
  • Fix Dropdown list cannot be created with values.
    • Fixed Dropdown custom field options not being addable / saving as empty.
      Creating a "Dropdown" custom field showed the "List Options" box, but pressing
      Enter (or typing and saving) never added any option, and on the card the only
      selectable value was (none). The custom-fields sidebar component was migrated
      from BlazeComponent to a plain Template, but the template still iterates the
      options with {{#each dropdownItems.get}} — under BlazeComponent that resolved
      to the instance's dropdownItems ReactiveVar, whereas a plain Template does not
      expose instance variables to the template, so the list rendered nothing. Because
      the options were never rendered as inputs, getDropdownItems() then overwrote
      the ReactiveVar with the empty DOM on save and every entered value was dropped.
      Fixed by re-adding the missing dropdownItems helper (returning the ReactiveVar),
      matching the pattern the other migrated templates already use. Thanks to Claude.
    • Fixed Exception in global helper _ when opening the Create Custom Field
      popup (and any translation containing a literal %).
      i18n is configured with
      a global sprintf post-processor (postProcess: ["sprintf"]), so every
      translation is run through i18next-sprintf-postprocessor. The help text
      custom-field-stringtemplate-format ("Format (use %{value} as placeholder)")
      contains a literal %{value} that sprintf cannot parse, so vsprintf threw and
      crashed the global Blaze _ translation helper — breaking that popup and any
      string (in any language, including user translation overrides) that contains a
      stray %. TAPi18n.__ now retries without the sprintf post-processor when it
      throws, returning the raw string (so %{value} is shown literally) instead of
      crashing.
      Thanks to rouceto1, xet7 and Claude.
  • Fixed new checklists (and checklist items) on a newly added card not being visible until logout/login.
    The board publication batched checklists,
    checklist items, comments and attachments into board-level cursors filtered by
    cardId: { $in: cardIds }, where cardIds was a one-time snapshot. In
    reywood:publish-composite a child cursor only re-runs when its parent (the
    board) document changes, so the snapshot never refreshed when a card was added
    — a checklist on a card created after subscribing matched no published card and
    only appeared on the next subscribe (logout/login). This was a regression from
    the "Optimized board loading" change, which had replaced the original reactive
    per-card child cursors with these batched snapshots. Fixed by denormalizing a
    boardId field onto Checklists and ChecklistItems
    so they can be
    published with a single board-level cursor filtered by boardId — one cursor
    per collection (keeping the load optimization) that still reacts to checklists
    on newly added cards, because a new checklist is created already carrying the
    board's id. boardId is set on insert (server before.insert hooks, plus
    explicitly in the Trello/WeKan board importers, which use direct.insert and
    bypass hooks), re-derived when a checklist/item or its card moves to another
    card (before.update on cardId) or the card moves to another board
    (Cards.after.update cascade), and backfilled for existing data by an
    idempotent startup migration. New { boardId: 1 } indexes were added on both
    collections. Comments and attachments instead remain reactive as per-card
    children of the cards cursor. Assigned-only board members still only receive
    checklists for cards assigned to them (the board-level cursor falls back to the
    assigned cards' ids for those roles).
    Thanks to ahlgrimma, xet7 and Claude.
  • Fixed SyncedCron crash.
    Fixed deleting archived lists (or many cards) crashing the server with
    SyncedCron: Fatal error encountered (unhandledRejection): TypeError: Cannot read properties of undefined (reading 'boardId') at
    server/models/checklistItems.js.
    Deleting a list cascades into removing its
    cards, and each card's checklists, checklist items, comments and attachments
    (cardRemover in models/cards.js). Cards.before.remove called cardRemover
    without await (and was not async), so the card document was deleted
    first and the cascade then ran with the parent card already gone; the
    ChecklistItems.before.remove / Checklists.before.remove hooks dereferenced
    the now-undefined card (card.boardId) and threw, and because the promise was
    unhandled, SyncedCron caught the rejection and tore down all running cron jobs.
    Fixed by making Cards.before.remove async and awaiting cardRemover (so
    sub-items are removed while the card still exists), and the REST card-delete
    handler now runs cardRemover before removing the card. As defense in depth,
    every card-activity hook and helper that looked up a card and used its
    boardId / listId / swimlaneId now skips (with a warning) when the parent
    card — or, for checklist-completion activities, the parent checklist — is
    missing, instead of throwing: before.remove on checklist items and
    checklists, Checklists.after.insert, the Cards.before.update timing
    activity, and the shared itemCreation / publishCheckActivity /
    publishChekListCompleted / publishChekListUncompleted / commentCreation
    helpers (matching the guards already present in
    server/models/cardComments.js).
    Thanks to titver968, xet7 and Claude.
  • Fixed upgrade crash
    An error occurred when creating an index for collection "users": Topology is closed / MongoServerSelectionError: Server selection timed out after 30000 ms.
    When WeKan started before MongoDB was reachable
    and had an elected replica-set primary (common right after an upgrade, while
    MongoDB replays its WiredTiger journal, or when the app container starts at the
    same instant as the database), the first index creation threw and Node exited.
    Now:
    • WeKan waits until MongoDB is ready (reachable, with an elected primary)
      before it starts, in every launch path: start-wekan.sh, start-wekan.bat,
      the Snap (snap-src/bin/wekan-control), and the app itself
      (server/00waitForMongo.js / server/lib/mongoStartup.js, which blocks the
      first Meteor.startup so it also protects the plain Docker image). The Docker
      docker-compose.yml and docker-compose-multitenancy.yml now give wekandb
      a healthcheck (primary elected) and the WeKan/tenant services
      depends_on: condition: service_healthy.
    • If MongoDB stays unreachable for too long (default 120s, configurable via
      WEKAN_DB_WAIT_TIMEOUT), a clear English-only message is printed to the
      Node.js console / docker logs / snap logs explaining that a database
      upgrade with mongodump (old MongoDB) and mongorestore --drop (new
      MongoDB) may be needed, and reminding that attachments and avatars live
      on disk under WRITABLE_PATH (files, attachments, avatars; on Snap
      /var/snap/wekan/common/files, on Docker the wekan-files volume at
      /data) and must be copied too. WeKan keeps retrying after printing it.
    • Index creation is now idempotent and crash-safe. A new ensureIndex
      helper checks the existing indexes and only creates the ones that are
      missing, and never throws — a single index problem is logged in English
      instead of taking the whole server down. All startup index creation across
      the model files was switched to it.
      Thanks to xet7 and Claude.
  • Fixed OpenAPI REST API documentation generation,
    which had been broken
    since after WeKan v7.93 and only generated docs for the login/register
    endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
    the REST routes from models/*.js (JsonRoutes.add(...)) into
    server/models/*.js (WebApp.handlers.get/post/put/delete(...)) and
    introduced optional chaining (?.) that the esprima Python parser cannot
    read, so openapi/generate_openapi.py silently skipped every route file.
    The generator now understands both routing styles, scans both models/ and
    server/models/, downlevels modern JS syntax so files parse, handles the
    type: Array SimpleSchema idiom, and releases/rebuild-docs.sh works directly
    with Python 3.12.x (PEP 668). The generated public/api/wekan.yml /
    wekan.html now cover the full API again (89 operations / 61 paths).
    Thanks to xet7 and Claude.
  • The attachment copy API now honours the admin "Admin Panel / Attachments"
    API transfer limits.
    Copying an attachment creates a new attachment but
    skipped the apiUploadBlocked / apiUploadMaxBytes checks that upload
    enforces; copy now respects them in both API implementations. Thanks to Claude.
  • Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing.
    Several issues in the bulk move:
    • The GridFS source matcher required meta.gridFsFileId to be absent, but
      Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
      files and "nothing happened". The matcher now recognizes real GridFS files
      (versions.*.storage === 'gridfs' or a meta.gridFsFileId reference), and a
      "nothing to move" message is shown when a source is empty instead of silently
      doing nothing.
    • Moving files crashed the whole server with
      FilesCollection#findOne() not available in serverReactiveCache.getAttachment
      used a synchronous findOne(); it now uses findOneAsync(). The background
      job is also hardened so a single failing file is skipped instead of crashing
      the server via an unhandled rejection.
    • The attachment copy API now honours the admin API upload limits (it
      previously skipped them).
  • Fixed the "MongoDB Meteor-Files" file-count statistic in Admin Panel /
    Attachments, which counted every attachment metadata document (so files on
    the Filesystem were wrongly reported as being in GridFS). It now counts only
    attachments actually stored in GridFS, consistent with the move tool. Also
    renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
  • Read legacy CollectionFS attachments and avatars in place (without
    migrating). The backward-compatibility layer
    (models/lib/attachmentBackwardCompatibility.js) was broken — it looked up the
    GridFS binary by the filerecord _id and by filename instead of by
    ObjectId(copies.<coll>.key), so legacy files were never found. It is fixed and
    generalized for attachments and avatars. Legacy attachments now appear in the
    card attachment gallery (new legacyBoardAttachments publication) and stream
    from the cfs_gridfs.attachments bucket, and legacy avatars
    (/cfs/files/avatars/<id> URLs) are served from the cfs_gridfs.avatars
    bucket instead of redirecting to a 404.
    Thanks to xet7 and Claude.
  • Ask to install npm dependencies before running tests.
    Thanks to xet7 and Claude.

Thanks to above GitHub users for their contributions and translators for their translations.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track wekan

Get notified when new releases ship.

Sign up free

About wekan

The Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . New English strings for new features at imports/i18n/data/en.i18n.json . Non-English translations at https://app.transifex.com/wekan/wekan only.

All releases →

Related context

Beta — feedback welcome: [email protected]