8982 Commits
Author SHA1 Message Date
b962dfbdb1 fix(gantt): cascaded issues and the critical path keep their working-day duration (#11065)
* fix(tracker): cascaded issues keep their working-day duration

In working-days mode simulateCascade placed a shifted issue's anchor on a
working day but moved its other end by the same number of calendar
days, so the issue kept its calendar-day length and could end on a
weekend or a holiday. A pushed successor and a pulled predecessor now
keep their length in working days (workingDaySpan, never below 1); the
free end is dueForSpan / startForSpan of the anchored end, for all four
relation kinds. The anchor, the FS gap floor and the violation check are
unchanged, and without a calendar the arithmetic is byte-identical.

Test updates: the chain push from #11054 now ends B on Mon 19 (two
working days) and C on Tue 20 - Wed 21; the multi-hop floor test uses a
Monday primary so the floor still dominates the second hop; the pull
test uses a two-working-day predecessor and keeps its expectation.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(tracker): critical path preserves working-day durations

computeCriticalPath preserved each issue's duration in milliseconds, so
with a working-days calendar the latest start of a predecessor rolled
back across a weekend was too early and its slack too large. The forward
and backward passes now keep the working-day span (endFor / startFor via
dueForSpan / startForSpan) when a calendar is active, so LS/LF match what
the cascade would produce. Legacy mode keeps the millisecond duration;
slack stays in milliseconds.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(tracker): address review — no silent truncation of long bars, critical-path coverage, doc placement

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-13 17:18:44 +02:00
963b1765a0 fix(github): keep OAuth codes, state and tokens out of logs (#11066)
* fix(github): keep OAuth codes, state and tokens out of logs

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(github): sanitize OAuth errors before analytics and logs

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-13 13:26:12 +07:00
1870da7f6f auth-providers: OIDC callback hardening (#10931)
* fix(authProviders): instrument OIDC callback + redirect on fail (not 500)

Production-blocking: every OIDC callback (bogus or real code) returned
500 Internal Server Error, even with valid sessions. No log was emitted
between `try auth via openid` and the 500 — passport strategy threw
silently before `handleProviderAuth` could log.

Per Codex plan-review amendments:
- Switch to explicit-callback passport.authenticate variant so strategy
  errors surface as err/info/status args instead of being swallowed.
- Privacy-safe diagnostic shape (no raw code/state/tokens/user). Fields:
  stage, errMessage/Name/Stack, hasUser, infoSummary, statusCode,
  hasSession + sessionKeys (names only), cookie + state + code
  PRESENCE (lengths only), host, forwardedProto.
- Permanent invalid-callback guard: ALL failures → 302 to /login with
  ?error=oidc_callback_failed, never 500. Same wrap covers
  handleProviderAuth throws.

Image-tag: hardcoreeng/account:integration-wac-2026-06-21-codex-r7.

Next step (P0-T2): user attempts login, we capture the actual error
from the diagnostic, then ship a surgical fix in r8.

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(account): admin.ts trim+lowercase on both env-set AND lookup

Codex plan-review Critical: server/account/src/admin.ts:15-18 built the
ADMIN_EMAILS set without normalizing, AND isAdminEmail() didn't normalize
the input. Today value is clean so isAdmin works, but a future env-value
like ADMIN_EMAILS=" Michael@Uray.io " would silently break admin
detection (Set lookup would miss the lowercased input).

Mongo backend already has this hardening (collections/mongo.ts:1141-1147).
Postgres + this helper didn't.

Now BOTH sides normalize: env split -> trim -> toLowerCase -> filter
non-empty + warn on no-@ entries (Codex Optional: warn not reject --
too sharp for a latent fix). Lookup also trim().toLowerCase().

Standalone upstream-PR-able per Codex Q-I3.

Tests cover whitespace, case-insensitive env, case-insensitive lookup,
invalid-shape warn, empty/unset env, multi-entry.

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* chore(authProviders): drop cookieHeaderLen from OIDC diagnostics (E4 amendment)

Codex E4 amendment (optional, low-priority): hasCookieHeader is
sufficient to diagnose missing session-cookie issues; the
cookieHeaderLen byte count adds no operational value and is one more
PII-adjacent surface to keep an eye on. Drop it from both diagnostic
paths in pods/authProviders/src/openid.ts.

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* auth(oidc): gate verbose callback diagnostics behind OIDC_DEBUG (L-AUTH-2)

Bei jedem fehlgeschlagenen OIDC-Callback wurde ein umfangreicher Diag-Block
(errStack/sessionKeys/host/…) auf error-Level geloggt -> anonymes Log-
Flooding via wiederholter invalider Callbacks. Normalbetrieb loggt jetzt nur
eine knappe Fehlerzeile; der volle Diag-Block nur mit OIDC_DEBUG=true.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* account(admin): drop non-email ADMIN_EMAILS entries unless explicitly allowed (L-AUTH-3)

Ein Tippfehler wie ADMIN_EMAILS=admin,michel@... legte bislang einen
unbeabsichtigten Admin-Identifier an (Eintrag ohne @ wurde still behalten).
Jetzt werden @-lose Eintraege per Default verworfen (fail-closed) und nur
bei ADMIN_EMAILS_ALLOW_LOGIN_ID=true als Login-ID akzeptiert; Warnung stets.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* auth(oidc): redirect to /login when provider auth yields no account (L-AUTH-4)

handleProviderAuth liefert '' bei einem nicht aufloesbaren Login (z.B. kein
Account + Signup disabled). Bisher erfolgte dann kein Redirect und next()
lief ins Leere -> haengende/leere Antwort. Jetzt fail-closed: explizites
302 auf /login?error=oidc_no_account.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* test(account): use example.com fixtures in adminEmails tests

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* auth(oidc): retry strategy registration with capped backoff (L-AUTH-5)

The account service registered the OIDC passport strategy only once at
startup via a single fire-and-forget Issuer.discover(). A transient issuer
outage in that one boot moment left the 'oidc' strategy permanently
unregistered, producing 500 "Unknown authentication strategy" on every
OpenID login until a manual container restart (observed: ~10 days).

Make registration self-healing:

- Extract registerOidcStrategyWithRetry(), which retries the ENTIRE
  registration chain (discover -> new Client -> new Strategy -> passport.use)
  with capped exponential backoff (5s, x2, cap 60s), unbounded. The loop only
  returns after passport.use succeeds, so a failure at client or strategy
  construction (e.g. temporarily incomplete issuer metadata during an IdP
  upgrade) is retried too instead of permanently disabling OIDC. discover and
  sleep are injectable for tests; passport is a structural { use } interface.
  It never rejects: the success log runs failure-safe AFTER passport.use and
  returns immediately, so a throwing logger can never re-enter the loop and
  register the strategy a second time; the failure warn is likewise guarded so
  a throwing sink cannot break the never-rejects contract.

- Gate log flooding via shouldWarnOnAttempt(): warn on the first 5 attempts
  and every 60th thereafter (~24 warn lines/day at the 60s cap instead of
  ~1440), following the branch's L-AUTH-2 rationale; error stacks only under
  OIDC_DEBUG. Success logs once with the attempt count.

- Wire it fire-and-forget in registerOpenid() and flip an oidcReady flag on
  success. The .then body is a plain boolean flip that cannot throw; a trailing
  .catch(() => {}) is belt-and-suspenders so this fire-and-forget chain can
  never surface an unhandled rejection. Routes stay registered synchronously.

- During the pending window, /auth/openid responds 503 with Retry-After: 5
  and a short text body instead of a 500 or a redirect (the login app ignores
  an error query param, and L-AUTH-4 only covers the callback path).

Add the package's first tests: the retry helper in isolation (schedule, 60s
cap, whole-attempt retry incl. client-construction failure, warn gating,
OIDC_DEBUG both ways, a throwing success log that must not re-register, and a
throwing failure warn that must not reject) and the registerOpenid wiring
(synchronous route registration, 503 pending window, exactly-once strategy
registration).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* account(admin): keep non-email ADMIN_EMAILS entries by default; strict mode opt-in

Review feedback on #10931: dropping entries without "@" by default would
silently revoke admin rights on upgrade for deployments that use login-id
style values. The repo's own dev/tests/ws-tests compose files ship
ADMIN_EMAILS=admin,${PLATFORM_ADMIN_EMAILS} and tests/prepare-pg.sh creates
that `admin` account.

Restore the original semantics as the default: every non-empty entry is
kept (trimmed + lowercased), and entries without "@" are logged once at
startup so a typo stays visible. The fail-closed behaviour becomes an
explicit opt-in via ADMIN_EMAILS_STRICT=true. ADMIN_EMAILS_ALLOW_LOGIN_ID,
introduced earlier on this branch and never released, is removed so there
is a single switch.

Empty entries stay dropped. This is a deliberate tightening compared to
the original `new Set(ADMIN_EMAILS.split(','))`, which put '' into the set
for any empty entry (ADMIN_EMAILS='', ',admin', 'admin,', 'admin,,ops@...')
and so made isAdminEmail('') true. loginOrSignUpWithProvider passes '' when
the provider returns no email, so such a login received an admin token.
isAdminEmail('') is now always false.

Tests: default keeps `admin` and warns, strict drops, only the exact value
"true" enables strict mode, no warning for clean values, empty entries in
every position never make an empty email an admin, trim/lowercase coverage
unchanged. Each test saves, clears and restores ADMIN_EMAILS and
ADMIN_EMAILS_STRICT so none depends on the outer environment.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* auth(oidc): settle the passport callback wrapper on rejection; address review

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* auth(providers): pass only provider-verified emails; abort OIDC retry on shutdown; more callback tests

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-13 13:23:33 +07:00
Artyom SavchenkoandGitHub 5bb9b2fea6 Redesign Gant charts and fix issues (#11058)
* Redesign Gant charts and fix issues

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix svelte check

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix review comments

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Add missing translations

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
2026-10-07 15:31:15 +02:00
d10b0a1414 feat(gantt): snap single issue bar drags, resizes and drops to working days (#11060)
* feat(gantt): working-day span helpers and prevWorkingDay

Add prevWorkingDay (mirror of nextWorkingDay), findWorkingDay (the
nearest working day in a direction, or undefined when none lies within
the 60-day search window, so a caller can tell a failed search from a
result), workingDaySpan (inclusive working-day length of a bar, never
below 1), dueForSpan / startForSpan (the far end of a bar with a given
working-day span) and workingDaysPerWeek (active weekdays of the mask).

The helpers stay bounded for degenerate input: non-finite or inverted
ranges count as one working day, ranges, spans and addWorkingDays step
counts are capped at MAX_WORKING_SPAN_DAYS (about 100 years), and
addWorkingDays treats a non-finite step count as zero instead of looping
forever.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* feat(gantt): snap drag, resize and drop previews to working days

reduce() takes an optional WorkingCalendar. With a calendar, a single-bar
body drag lands the start on the nearest working day in the drag direction
and keeps the bar's length in working days; the start handle rounds up to
the next working day and the end handle rounds down to the previous one;
a dropped unscheduled issue starts on the next working day and lasts two
working days. A zero-delta move leaves the bar untouched. A co-drag keeps
its shared raw delta, and without a calendar the reducer is unchanged.

Degenerate calendars fall back to calendar days: a calendar without any
working weekday, a bar whose origin is not a finite range of at most
MAX_WORKING_SPAN_DAYS days, and a preview with no working day within
reach (a holiday blackout longer than the 60-day search window), so a
non-working day is never presented as a snapped result.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* feat(tracker): snap Gantt drags to the project working days; move children and keyboard shifts by working days

GanttView passes the project calendar to the drag reducer for single
issue bars; milestones and bulk co-drags keep calendar-day drags. A
parent drag shifts its children by the parent's move measured in
working days (shiftWithPrimary), and the arrow keys move a bar by
working days (shiftScheduleDays), with Shift moving one week = the
number of active weekdays (keyboardWeekStep). The help overlay now
says "±1 week". Without a project calendar every path keeps its
calendar-day arithmetic.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* feat(gantt): hold Shift while dragging to ignore working days

Holding Shift during a single issue bar drag, resize or sidebar drop
suspends working-day snapping for that gesture: the preview and the
committed dates move in calendar days, exactly as in a project without
working days, and the children of a dragged parent shift by the same
calendar-day delta. Pressing or releasing Shift mid-drag updates the
preview at once, so the user can decide before dropping. A "Calendar
days" hint is shown under the date pill while snapping is suspended.
The commit uses the modifier state of the release event itself, and
losing window focus counts as releasing Shift, so a Shift let go outside
the window cannot leave the drag committing calendar days.

Shift is the free modifier during a drag: Alt is already read at release
to bypass the cascade simulation (and Alt+drag is grabbed by several
Linux window managers), Cmd/Ctrl toggle the selection, and Shift only
range-selects on a plain click. A body drag still has to start without
a modifier; Shift is pressed once the bar moves.

The reducer takes `calendarDays` on `mousemove` and marks the state
`snapSuspended` only when a calendar would otherwise apply; legacy mode,
co-drags and milestones are unchanged. `dragCalendar(state, calendar)`
gives the commit the calendar for the drag's own arithmetic.
`modifierSyncMove(state, calendarDays, lastMove)` yields the move that
re-syncs a running drag with the modifier at the last pointer position.
Dependency cascades keep following the project calendar. The keyboard
help lists the new shortcut; strings are added for all tracker locales.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* test(gantt): make the working-day cap tests deterministic

The addWorkingDays cap test asserted a wall-clock bound (< 2000 ms) that
failed on a slower CI runner. Assert the exact results instead: each loop
step moves the cursor by one calendar day, so the distance of the result
from the input is a deterministic count of the iterations. Cover the
safety bound of calendars without working weekdays with small step counts
and drop the costly 70-holiday walk, which cuts the suite's slowest tests
from ~950 ms to ~250 ms.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-07 10:50:38 +02:00
5077d97413 fix(account): never log raw tokens, report only unexpected token failures (#11059)
* fix(account): never log raw tokens, report only unexpected token failures

getLoginInfoByToken and getLoginWithWorkspaceInfo logged the full token on
every validation failure and reported each failure to Analytics, although
expired or stale session tokens are routine. getWorkspacesInfo and
updateLastVisit logged the token when rejecting a non-system caller.

Log a keyed fingerprint instead of the credential (omitted when no server
secret is configured) together with a fixed failure reason. A TokenError is
logged at warn level and not reported to Analytics, as wrap() already
treats it. Any other error is logged at error level and reported as a
fresh error that carries only the error class name, never the original
message, stack, cause or custom fields. Status codes and responses are
unchanged.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* test(account): fix lint in token redaction tests

Reference the mocked Analytics.handleError through one local jest.Mock
per test file instead of passing the unbound method to expect(), which
tripped @typescript-eslint/unbound-method in the formatting job.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-07 13:02:31 +07:00
Artyom SavchenkoandGitHub d6d135340c Ignore skills in git (#11063)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
2026-10-06 21:29:54 +07:00
Artyom SavchenkoandGitHub 4527b0e4f0 Fix Svelte check on Windows (#11062)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
2026-10-06 16:04:26 +02:00
Artyom SavchenkoandGitHub 74e6e5d53d Windows development support without WSL (#11061)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
2026-10-06 19:42:06 +07:00
b426f096ec fix(gantt): don't pull a predecessor whose FS constraint is already satisfied (#11054)
In working-days mode the reverse anchor is not the inverse of the forward
anchor when the predecessor's anchor falls on a non-working day:
fsAnchor(Sat) = Mon, but fsReverseAnchor(Mon) = Fri. The reverse pass of
simulateCascade only compared the reverse anchor against the predecessor,
so after an FS push that left a successor ending on a Saturday, the next
link in the chain pulled that successor back by the calendar delta. The
pulled issue then started on its own predecessor's last day, and that
upstream link was never re-checked because the primary edit is guarded.

Check the violation with the forward anchor (the same predicate the
outgoing pass uses) before pulling. Legacy calendar-day mode is unaffected
because its forward and reverse anchors are exact inverses.

Tests cover the reported chain (A -> B -> C across a weekend), a successor
dropped on the first working day after a Saturday-ending predecessor, a
real violation that must still pull, and legacy-mode regressions.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-10-02 05:42:39 +02:00
Artyom SavchenkoandGitHub e4fd72b36a Fix unstable tests (#11052)
* Fix unstable tests

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix tests

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix tests

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix date formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-30 13:16:18 +02:00
Artyom SavchenkoandGitHub 798280391c Fix person invite (#11051)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-30 06:32:14 +02:00
Artyom SavchenkoandGitHub 90d418c224 Fix validation issues (#11050)
* Fix formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix svelte-check

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix build

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix print resources

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-30 05:12:59 +02:00
f030c2a337 fix: respect locale date order for Asian languages in DatePresenter #10779 (#10781)
* fix: respect locale date order for Asian languages in DatePresenter

Signed-off-by: Learnobug <gunjanaggarwal12345@gmail.com>

* fix: apply formatting fixes

Signed-off-by: Learnobug <gunjanaggarwal12345@gmail.com>

---------

Signed-off-by: Learnobug <gunjanaggarwal12345@gmail.com>
Co-authored-by: Artyom Savchenko <armisav@gmail.com>
2026-09-29 07:42:46 +02:00
Ignat RemizovandGitHub 7d01eb2cd3 fix(chunter): skip legacy comment migration when domain is absent (#10891)
Allow old Chunter migrations to complete cleanly for workspaces that no longer have the legacy `comment` storage domain. The migration still runs normally when the domain exists, but workspaces created or restored after the comment-to-activity migration can now skip the obsolete table operations and record the migration state.

Changes:
- Add a migration-client `domainExists` helper backed by the active adapter.
- Make the Postgres domain helper check actual public tables instead of assuming every domain exists.
- Keep Postgres domain listing compatible with model domain names while including translated table names.
- Guard legacy `comment` update/move/delete operations in the Chunter migration.

Validation:
- git diff --cached --check
- node common/scripts/install-run-rush.js build -t @hcengineering/model -t @hcengineering/server-tool -t @hcengineering/postgres -t @hcengineering/model-chunter

Signed-off-by: Ignat Remizov <ignat@ignatremizov.com>
2026-09-29 07:36:56 +02:00
ce3c4a1dc3 fix(chunter-trigger): seed default collaborators onto the target doc, not the message (#10973)
OnChatMessageCreated seeds a target doc's default collaborators when the doc
has none yet. The seed passed the ChatMessage create tx's own identity
(tx.objectId / tx.objectClass / tx.objectSpace) to getAddCollaboratTxes, so
the Collaborator records were attached to the ChatMessage instead of the
target doc. Two consequences:

- The target doc was never actually seeded: currentCollaborators stayed empty,
  so the defaults were lost and the seed re-evaluated on the next message.
- For provideSecurity classes the seed re-fired on every message and littered
  the message with Collaborator records.

Pass targetDoc._id / targetDoc._class / targetDoc.space instead.

This wrong-target is long-standing (pre-existing); #9217 only mechanically
inherited the line and did not introduce the bug.

Adds a from-scratch jest harness (chunter-resources shipped no tests). It
drives ChunterTrigger with a TxCreateDoc<ChatMessage> on an empty target and
asserts the seeded Collaborator attaches to the target doc
(attachedTo / attachedToClass / objectSpace). getAddCollaboratTxes runs for
real — only getDocCollaborators and createCollaboratorNotifications are
stubbed — so the assertion exercises the actual wiring: red before the fix
(attachedTo = message id), green after.

A sibling of this bug lives in server-notification-resources collectionCollabDoc
(the createCollabDocInfo seeding path); left out of scope here.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-09-29 07:29:52 +02:00
Artyom SavchenkoandGitHub 3fe94f85c2 Fix minio image (#11049)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-29 07:25:34 +02:00
Artyom SavchenkoandGitHub 488340fc31 Fix employee bypass (#11048)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-29 07:20:35 +02:00
Ignat RemizovandGitHub f4f3d30bf1 fix(contact): enforce employee contact edit permissions (#10890)
Enforce contact edit boundaries for regular users on both the UI and transaction middleware so users can maintain their own profile details without being able to change other employees' person records or communication channels.

Changes:
- Reject regular-user CUD transactions that update another employee Person or channels attached to that employee, including nested TxApplyIf payloads.
- Allow regular users to edit their own contact details and non-employee contacts, while Maintainers and Owners continue to manage employee records.
- Share contact editability logic in contact resources and use it in person editing, channel editors, and channel presenters so the UI hides or disables blocked actions.
- Add middleware tests covering own-person edits, blocked cross-employee person/channel edits, and maintainer bypass behavior.

Validation:
- git diff --check
- rushx svelte-check in plugins/contact-resources
- targeted Rush tests for @hcengineering/middleware and @hcengineering/contact-resources
- targeted Rush build for @hcengineering/middleware and @hcengineering/contact-resources

Behavioral effect:
Regular users can update their own profile contact details, but attempts to edit another employee's person record or attached email, phone, or GitHub channels are blocked even if sent directly to the server.
2026-09-26 05:07:17 +02:00
41f8b06ae9 fix(core): coalesce orphan-tx warnings in ModelDb.addTxes (#10927)
Per-TX 'no document found' warnings were previously emitted via ctx.warn
for every orphan-targeted transaction the loader skipped. On workspaces
that have evolved through multiple model versions this produces dozens
of identical-looking warnings on every page load, drowning out real
warnings without giving operators an actionable signal.

Replace with a single coalesced ctx.info summary at the end of addTxes:
total count plus a byClass breakdown. The information needed for
diagnosing migration issues is still present, just consolidated.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-09-26 05:01:22 +02:00
bd7378083f feat(account): add operator controls for sign-up and guest login (#10949)
* feat(account): DISABLE_SIGNUP + DISABLE_GUEST_LOGIN env vars + getLoginCapabilities

When DISABLE_SIGNUP=true on the account service, the signUp and signUpOtp
operations throw Forbidden — preventing manual sign-ups even via direct
API calls, not just hiding the UI tab.

When DISABLE_GUEST_LOGIN=true, the loginAsGuest operation throws Forbidden
without touching the database. Combined with the existing
AccountNotFound branch (no guest person row), the guest login is fully
controllable via env or DB state.

A new getLoginCapabilities operation lets the client query both flags so
the UI can hide the corresponding affordances. It reports
signUpEnabled (false iff DISABLE_SIGNUP=true) and guestLoginAvailable
(false iff DISABLE_GUEST_LOGIN=true OR no read-only guest person row
exists). The client RPC stub is added to AccountClient as well.

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* feat(login-resources): hide signup tab + guest button based on capabilities

LoginForm now queries getLoginCapabilities on mount and uses the result
to gate two UI affordances:

- effectiveSignUpDisabled OR-s the existing signUpDisabled prop with
  !signUpEnabled from the server, so the Sign Up tab disappears when
  the account service reports sign-up disabled (DISABLE_SIGNUP=true).
- The "Continue as guest" button is rendered only when
  guestLoginAvailable is true, which the server reports as false when
  either DISABLE_GUEST_LOGIN=true or the read-only guest person row is
  not present in the DB.

fetchLoginCapabilities returns { signUpEnabled: true,
guestLoginAvailable: true } on any RPC error to keep both controls
visible when talking to older account services without the endpoint.

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* chore: apply repo formatting (fast-format output)

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* account(login-capabilities): cache guest-person lookup to avoid anon DB probing (L-AUTH-1)

getLoginCapabilities ist unauthenticated und traf pro anonymem Aufruf die
DB (db.person.findOne auf den Guest-Account) -> Existenz-Probing + DB-Last.
Guest-Person-Zustand aendert sich praktisch nie: Ergebnis jetzt mit 60s-TTL
modul-lokal gecacht. resetGuestPersonCache() fuer Tests/Ops.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michael.uray@gmail.com>
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-09-25 18:44:35 +02:00
b043eb3f60 fix(notification-trigger): seed default collaborators onto the parent doc, not the attached tx (#10975)
getCollaborators resolved the default collaborators of the parent doc (loaded via the triggering tx's attachedTo/attachedToClass) but seeded them onto tx.objectId — the attached child item (a comment/message create/update/remove tx). The parent's collaborators therefore landed on the child record instead of the parent.

Seed onto doc._id/doc._class/doc.space instead. Same class of bug as the chunter seed-target fix. The returned collaborator list is unchanged; only the target of the newly created Collaborator records changes.

Adds a unit test that drives getCollaborators directly: real getAddCollaboratTxes/getDocCollaborators, only getClassCollaborators stubbed. Parent and child are given distinct collaborators and spaces; it asserts the seed targets the parent (attachedTo/attachedToClass/objectSpace), never the child, while the returned collaborator list stays identical.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-09-25 18:41:22 +02:00
Artyom SavchenkoandGitHub c1548a8a52 Cache providers for common notifications (#11047)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-25 18:39:28 +02:00
Artyom SavchenkoandGitHub afa747d065 Handle blocked audio and invalid VAPID keys (#11046)
* Handle blocked audio and invalid VAPID keys

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix build issues

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-25 18:34:36 +02:00
dhaval-patel262andGitHub c44df0ef7e fix(notification): pass matched types to common inbox notifications (#11027)
getCommonNotificationTxes() passed a hardcoded empty array as the `types`
argument to pushInboxNotifications(), so every notification created through
it was stored with `types: []`.

NotificationsHandler resolves the allowed providers per notification type.
With no types it matches no provider, logs "no notifications with email
provider, skipping" and returns, so these notifications can never be
delivered by email or any non-inbox provider.

This affects everything routed through this helper - ToDos, process
approvals (process:ids:ApproveRequestCreated), HR requests - while
activity-message notifications are unaffected because that path passes its
types through.

The types are already computed: isShouldNotifyTx() returns a NotifyResult
mapping each provider to the NotificationType docs that matched, and the
result is passed in as notifyResult. Forward the inbox provider's types
instead of discarding them.
2026-09-25 18:21:19 +02:00
67c3ebfaf7 fix(front): clean startup console noise (#10965)
* fix(front): clean startup console noise

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(love-resources): make content-type check strict-boolean safe

The recording availability probe used a negated optional chain
(!res.headers.get(...)?.includes(...)) whose type is boolean | undefined.
The repo ESLint config rejects this under
@typescript-eslint/strict-boolean-expressions and it breaks the
formatting CI job. Compare against true explicitly instead; behaviour
is unchanged (a missing or non-JSON content-type still means not JSON).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Artyom Savchenko <armisav@gmail.com>
2026-09-25 17:51:12 +02:00
DawnRatelandGitHub bda10fa45e feat(workbench): add optional workspace logos and color badges to browser tabs (#11035)
* feat: add optional workspace favicon and identification color

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* refactor: simplify workspace identification color control

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* fix: preserve logo size when overlaying favicon color

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* fix(workbench): separate favicon badge with a transparent cutout

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* fix(workbench): restore workspace favicon before app startup

Signed-off-by: Ratel-pwn <2448574643@qq.com>

---------

Signed-off-by: Ratel-pwn <2448574643@qq.com>
2026-09-25 11:00:19 +02:00
DawnRatelandGitHub 5eb50ee39b feat(print): add optional paginated DOCX previews (#11036)
* feat(print): add optional paginated DOCX previews

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* chore(print): align preview changes with repository checks

Signed-off-by: Ratel-pwn <2448574643@qq.com>

* style(print): normalize preview formatting

Signed-off-by: Ratel-pwn <2448574643@qq.com>

---------

Signed-off-by: Ratel-pwn <2448574643@qq.com>
2026-09-25 10:59:59 +02:00
Artyom SavchenkoandGitHub e749ab9d7b Frozen maintenance (#11045)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-25 05:09:55 +02:00
c864345e0f Add missing Turkish translations (#11042)
Signed-off-by: qwist1233-cpu <267938209+qwist1233-cpu@users.noreply.github.com>
Co-authored-by: qwist1233-cpu <267938209+qwist1233-cpu@users.noreply.github.com>
2026-09-22 08:35:52 +07:00
Artyom SavchenkoandGitHub 9aa7da386c Update README.md (#11037)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-09-17 20:09:29 +07:00
63e28dc964 feat(tracker): Gantt chart for the issue tracker (#10992)
* feat(tracker): Gantt chart for the issue tracker

Builds on the merged search/filter rework (#10998). Introduces @hcengineering/gantt, a dependency-free scheduling/zoom/drag/viewport engine package (with a neutral GanttDependency contract), and wires the tracker-specific adapter, Svelte UI, model schema, dependency-shift notifications and sanity coverage in tracker-resources.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(tracker): source Gantt holidays from HR and scope confirm-gate per view

Follow-up to review feedback on #10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. WorkingDaysConfig now carries only the weekday
  mask and an optional holidayDepartment. The tracker adapter resolves holidays
  for the selected department plus all ancestors up to hr.ids.Head, matching
  HR's existing inheritance semantics (hr-resources ScheduleView). A missing or
  invalid reference falls back to company-wide (root) holidays; unrelated
  departments are never included. The ancestor walk is cycle/missing-parent
  safe.

  A small WorkingDaysEditor in the existing project create/edit dialog lets
  users with the existing project permission enable the mode, choose weekdays,
  and select an HR department (default: Company-wide). It persists through the
  existing project create/update transaction paths (createDoc on create,
  client.update on edit) and uses $unset when the mode is disabled. At least
  one working day must stay active.

  Calendar loading is explicit via a CalendarStateMachine: project-scoped drag,
  resize, cascade, keyboard-shift and auto-schedule mutations stay disabled
  until both the project config and the holiday data have loaded. A monotonic
  generation ticket (bumped synchronously on project switch) is re-checked
  immediately before every calendar-dependent date commit, so work started in
  one project can never persist after a switch to another. The all-projects
  view has no single project calendar, so calendar-dependent edits are
  read-only there; display, selection, the context menu and the manual
  date-picker (which does no calendar arithmetic and takes no mutation ticket)
  stay available.

  This depends on the lightweight @hcengineering/hr model package, but not on
  hr-resources; the tracker adapter uses hr.class.* and hr.ids.Head at runtime,
  and actual HR model presence is checked through the hierarchy - without the
  HR model, holidays resolve to an empty set. The @hcengineering/gantt engine
  package stays domain-neutral (no @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* fix(gantt): measure FS gap-preservation floor in working days

The finish-to-start gap-preservation floor in the cascade scheduler mixed
two time measures: the FS snap anchor is working-calendar aware, but the
floor added a raw calendar-millisecond start-side delta. With an active
working calendar the two diverge, so dragging a predecessor over a weekend
over-shifted the successor across non-working days (a Fri->Mon body drag is
3 calendar days but only 1 working day, and could even land a successor on
a Saturday).

Measure the floor on the DUE side in working days instead: translate the
predecessor's shift via the new workingDayDelta helper and advance the
floor base with addWorkingDays over the calendar. A day-granular resize
gate (utcMidnight comparison, not a raw === 0 test) arms the floor only on
a start-DAY move, so a pure due-resize stays a pure snap even when the
stored issue carries a time-of-day on the cascade-commit path. The floor
base is normalized via utcMidnight(targetAnchor). The legacy path
(cfg === undefined) stays byte-identical to today's raw-ms floor. A
left-resize in working-days mode now correctly no-ops the floor (the due,
and thus the FS gap, did not move) instead of shifting on the raw start
delta.

Engine: add workingDayDelta (signed, half-open from-exclusive/to-inclusive
working-day step count) and export the previously-private utcMidnight from
packages/gantt/src/working-days.ts. The engine stays domain-neutral (no
@hcengineering imports, empty dependencies).

Adds engine and adapter test coverage: workingDayDelta cases and the full
FS floor matrix (right-resize no-push, weekend-spanner body drag, floor
wins over snap, holiday in the floor path, floor-dominant multi-hop chain,
negative delta, legacy no-cfg, SS pure-snap, raw time-of-day right-resize,
direct left-resize).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
2026-08-27 20:24:38 +07:00
1be6047c8a feat(tracker): field-prefixed search, inline filter chips and zero-hit empty state (#10998)
* feat(tracker): field-prefixed search, inline filter chips and zero-hit empty state

Adds SearchInputAdvanced (field:value prefixes routed to Elasticsearch query_string), match highlighting, inline filter chips with overflow popover, a reusable zero-hit empty state in view-resources, and search-scope/highlight view options for List and Kanban.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* refactor(tracker-search): address review follow-ups on search/filter rework

Shared, non-storage-specific field list, test-folder conventions, locale
coverage, single regex source and an owner-token gate for the result count.

- core: add shared `fullTextSearchFields` constant next to
  FullTextSearchContext as the single source of truth for the full-text
  fields exposed to `field:value` targeting. The client encoder derives
  ES_NATIVE_FIELDS from it and the elastic adapter derives KNOWN_FIELD_RE
  from it, replacing the two "KEEP IN SYNC" copies. Per-field boost weights
  stay a local adapter detail.
- tests: move the five co-located tests into each package's existing test
  folder convention (ui `__test__`, view-resources `__tests__`) and fix the
  relative imports.
- i18n: translate the new tracker search/filter strings in the remaining
  locales (zh, ja, ko, cs, es, fr, it, pt, pt-br, tr), reusing each file's
  existing terminology; ICU placeholders left unchanged.
- encoder: hoist the reserved-character class into one constant and build two
  RegExp instances from it (non-global for `.test()`, global for `.replace()`)
  to avoid the shared-lastIndex trap.
- view-resources: guard `resultIssueCountStore` writes with an owner-token
  gate so a superseded viewlet can no longer clobber the active viewlet's
  count; List/KanbanView claim and release, IssuesView resets through the
  current owner. Assumes a single active IssuesView surface.
- view-resources: make result-count reporting opt-in via a new
  `reportResultCount` prop on List (default true). Embedded, non-primary List
  instances (sub-issues / related issues in the issue edit panel, routed
  through SubIssueList) pass false and never claim the owner token, so opening
  and closing an issue can no longer strand the primary Issues viewlet with a
  dead token — the zero-hit SearchEmptyState card renders again afterwards. Add
  a regression test covering the opted-out embedded consumer.
- elastic: escape every regex metacharacter (not just `.`) when building
  KNOWN_FIELD_RE from `fullTextSearchFields`, so a future field name carrying
  another metacharacter cannot silently corrupt the alternation. Behaviour for
  the current fields is unchanged.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

* refactor(tracker-search): centralize regex escaping and lock down result-count store

- Share escapeRegExp from @hcengineering/core so the client encoder and the server elastic adapter escape the fulltext field list identically, not just dots on the client.

- Export resultIssueCountStore as a read-only Readable; the owner-token gate functions (setResultCount / resetResultCount / releaseResultCountOwner) are now the only write path.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>

---------

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Artyom Savchenko <armisav@gmail.com>
2026-08-11 19:09:33 +07:00
Don KendallandGitHub 5a3d673e84 feat(account): revocable API tokens managed from account settings (#10624)
* feat: API token management in workspace settings

Add UI and backend support for creating, listing, and revoking
API tokens scoped to workspaces. Includes owner-level workspace
token visibility, OpenAPI documentation, Mongo/Postgres persistence,
and i18n translations.

Signed-off-by: Don Kendall <kendall@donkendall.com>

* feat: enforce API token revocation at transactor level

Embed apiTokenId in JWT extra field and add a per-token revocation
cache (60s TTL) in the transactor REST handler. Revoked tokens are
now rejected within ~60 seconds instead of remaining valid until
JWT expiry.

Adds checkApiTokenRevoked account service method for the transactor
to query individual token revocation status.

Signed-off-by: Don Kendall <kendall@donkendall.com>

* feat: implement Phase 1 API token scopes (read/write/delete)

Add coarse-grained scope enforcement for API tokens. Tokens can now
be created with scopes ['read:*'], ['read:*','write:*'], or
['read:*','write:*','delete:*']. Existing tokens without scopes
retain full access (backward compatible).

- DB: v26 migration adds scopes TEXT[] column to api_tokens
- Types: add scopes field to ApiToken and ApiTokenInfo
- Operations: createApiToken accepts/validates/persists scopes,
  embeds in JWT via extra.scopes
- Enforcement: withSession checks scopes against method; tx handler
  additionally requires delete:* for TxRemoveDoc
- Client: createApiToken signature accepts optional scopes param
- UI: scope preset dropdown in create popup (default: Read Only),
  permissions column in token list with i18n labels
- Also fixes 3 pre-existing TS2322/TS2345 errors in operations.ts

Signed-off-by: Don Kendall <kendall@donkendall.com>

* test: add unit tests for API token scope enforcement

- scopes.test.ts: 8 tests for hasScope() and getRequiredScope() logic
- apiTokenScopes.test.ts: 7 tests for createApiToken scope validation
  (valid scopes, multiple scopes, no scopes backward compat, invalid
  format rejection, empty array rejection, domain-scope rejection)
  and listApiTokens scopes inclusion
- Export hasScope/getRequiredScope from rpc.ts for testability

Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: address review feedback — role restriction, locale parity, formatting

- Restrict API token creation/revocation to AccountRole.User or higher
  (guests cannot use API tokens), per reviewer suggestion
- Add 5 missing translation keys (ApiTokenPermissions, ApiTokenScopePreset,
  ApiTokenScopeReadOnly, ApiTokenScopeReadWrite, ApiTokenScopeFullAccess)
  to all non-en locale files to fix locale parity CI test
- Fix prettier formatting in apiTokenScopes.test.ts
- Rename local `extra` to `tokenExtra` in createApiToken to avoid
  shadowing the decoded token's `extra` field

Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: address aonnikov architectural review feedback

- rpc.ts: use system service token for checkApiTokenRevoked so the
  revocation check is not coupled to the user's potentially-revoked
  bearer token; systemAccountUuid + service:'server' ensures account
  service always accepts the call
- ApiDocsSection.svelte: derive transactor base URL from
  login.metadata.LoginEndpoint (set on auth) instead of
  window.location.origin, which is not necessarily the transactor host
- ApiTokenCreatePopup.svelte: replace manual translate() calls and
  themeStore language watch with DropdownLabelsIntl + DropdownIntlItem[],
  which handle i18n automatically; error state is now IntlString
- General.svelte: remove legacy GenerateApiToken button, handler, and
  ApiTokenPopup import in favour of the new ApiTokens settings panel

Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: formatting in ApiTokenPopup, apiTokenScopes test, and operations

Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: apply rushx fmt to pass CI formatting check

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* fix: restore (s as any) cast in server_http.ts removed by ESLint autofix

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* refactor(token): centralize API token revocation/expiry in verifyToken

Address @aonnikov's review: the bespoke checkApiTokenRevoked RPC and the
ad-hoc revocation cache in the transactor are replaced by a reusable
verifyToken in server-token that checks signature, expiry, and (for
revokable API tokens) revocation via a pluggable checker.

- server-token: add verifyToken + isTokenExpired + setApiTokenRevocationChecker
  (the 'method to verify' metadata the plugin needs, without depending on the
  account client). Revocation cache (60s TTL) now lives here, reusable by any
  service (transactor, blob access, etc.).
- account: the account is now authoritative — wrap() rejects revoked/expired
  API tokens, so any account method (selectWorkspace/getWorkspaceInfo/...)
  naturally 401s. Removed the redundant checkApiTokenRevoked method.
- account-client: drop checkApiTokenRevoked.
- transactor: withSession uses verifyToken; the registered checker reuses the
  existing getLoginInfoByToken instead of a dedicated boolean RPC. Scopes are
  parsed once and threaded through (no re-decode in the tx handler).
- tests: verifyToken/isTokenExpired unit coverage.

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* fix(setting): derive REST API base from account-provided transactor endpoint

Address @aonnikov: the REST API host must come from the transactor endpoint
returned by the account service (the login endpoint, a ws(s):// URL), not be
constructed from window.location. Convert ws->http and append /api/v1, matching
the existing ServerManagerGeneral pattern.

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* i18n(setting): translate API token strings across all locales

Address @ArtyomSavchenko: the new API token UI strings were left in English
in non-en locales. Provide translations for ru/de/es/fr/it/pt/pt-br/zh/ja/cs/tr.
The en/ru locale-parity test passes (the prior failure was an en/ru key
mismatch, resolved by the develop merge).

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* style: wrap long lines to satisfy prettier (account utils import, ApiDocsSection)

Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* fix(api-token): drop unenforceable scopes, harden the token lifecycle

The scopes were only ever consulted in the transactor's REST handler, so
they promised a boundary the platform did not keep:

- the WebSocket transport decodes the token and never looks at scopes, so
  a read-only token could open a socket and issue any transaction;
- even on the REST path, delete:* only matched a top-level TxRemoveDoc and
  was bypassed by nesting the removal in a TxApplyIf;
- nothing stopped a scoped token from calling createApiToken and minting
  an unscoped one.

Narrowing a token's rights has to happen in the pipeline, where it covers
every transport, and that is a larger design than this feature. Until then
a token carries its account's rights and says so, rather than displaying a
restriction that does not hold. Scopes are removed end to end.

What is kept is made to work:

- API tokens can no longer create, list or revoke API tokens. Otherwise a
  leaked token renews itself and revokes the tokens meant to stop it.
- Revocation fails closed. The account is the only authority on it, so an
  unreachable account now rejects the token instead of admitting a revoked
  one to whoever can keep the account busy. Verdicts stay trusted for the
  cache TTL so brief outages do not cut off healthy tokens.
- The revocation cache is bounded and re-checks negative verdicts.
- Revoking your own token no longer requires a role in its workspace, so
  leaving a workspace cannot strand a credential you can never revoke.
- The per-account limit counts only usable tokens; revoked and expired
  ones are kept for the audit trail and used to lock out anyone rotating.
- Rejected REST tokens are logged with a reason, since expired, revoked
  and unverifiable are one opaque 401 from outside.

The unused listWorkspaceApiTokens/revokeWorkspaceApiToken pair is removed;
it had no caller and no test.

Tests cover the role restriction, the API-token guard, ownership on
revoke, the limit accounting and fail-closed revocation. Removing any of
those checks fails them.

Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7
Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* fix(setting): correct the API token settings page

- Moves the page from workspace settings to account settings. Tokens
  belong to the account: the page already lists them across every
  workspace, and creating one only needs the User role the account
  service checks, not Owner as the category required.
- Surfaces failures. A failed revoke was logged to the console and the
  row re-rendered unchanged; loading workspaces could reject unhandled
  and leave an empty dropdown with no explanation.
- Outside a secure context there is no clipboard API, so the OK button
  did nothing and the dialog had no way out but Cancel. It now closes,
  leaving the token selectable.
- Replaces hardcoded English labels, adds aria-expanded on the docs
  disclosure and labels on the copy targets.
- Fills in the Korean and Polish translations, which were the only two
  locales missing these keys, and drops the API access strings orphaned
  when this PR replaced the old Generate API token button.

Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7
Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* chore: drop docs/openapi.yaml from the API token PR

Unrelated to this feature and wrong for this repo: it documents a
/_tokens minting service and a tools/mint-token CLI that do not exist
here, uses reverse-proxy prefixes from a self-hosted deployment rather
than the transactor's /api/v1 routes, and states that revocation is a
future enhancement, which is what this PR implements. Nothing references
it. REST API docs belong in their own change, generated against the
routes that exist.

Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7
Signed-off-by: Don Kendall <dkendall@ledoweb.com>

---------

Signed-off-by: Don Kendall <kendall@donkendall.com>
Signed-off-by: Don Kendall <dkendall@ledoweb.com>
2026-08-04 13:10:48 +07:00
Artyom SavchenkoandGitHub 8c9df54eb0 Add script to verify all backup data is present in a workspace (#10999)
* Add script to verify all backup data is present in a workspace

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Verify blobs exist

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-08-01 07:35:39 +07:00
Artyom SavchenkoandGitHub b056667372 Fix rekoni temp files clean up (#11000)
* Fix rekoni files clean up

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix copyright

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-08-01 07:35:24 +07:00
Artyom SavchenkoandGitHub 9b607d36ea Fix cross-workspace blob/thumbnail and metadata disclosure (#10972)
* Fix cross-workspace blob/thumbnail and metadata disclosure

The preview pod's thumbnail and metadata routes did not validate the
caller's token against the workspace taken from the URL.

Reported-by: Ugur Ozer, Aeon AI Risk Management

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Update copyright year

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Use secure mode

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix formatting

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Remove uuid check

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-28 15:56:18 +07:00
Artyom SavchenkoandGitHub de784bf2ef Clean up commented tools (#10993)
* Clean up commented tools

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-27 11:54:07 +07:00
Don KendallandGitHub afe11faec1 fix(account): authorize mergeSpecifiedPersons / canMergeSpecifiedPersons (#10997)
* fix(account): authorize mergeSpecifiedPersons / canMergeSpecifiedPersons

Both operations decoded the caller token and discarded the result, so any
authenticated caller could merge any two persons by uuid. Merging re-points
the secondary person's social ids onto the primary one, and neither login
nor password recovery require a social id to be verified, so this reached
as far as taking over an arbitrary account.

Authorize both behind verifyMergePersonsAuthority:

- global admin tokens and the tool/workspace services pass, matching the
  account level mergeSpecifiedAccounts;
- everybody else must maintain the workspace their token carries, and both
  persons must be within its reach: a person holding an account elsewhere,
  and the platform wide system and guest accounts, are refused;
- a login capable social id may not move onto an account the caller does
  not own. doMergePersons only refuses verified secondary social ids, which
  leaves the unverified ones a maintainer could mint for themselves.

canMergeSpecifiedPersons answers false instead of throwing: it is the
predicate the merge dialog polls, and it awaits it without a catch.

Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7
Signed-off-by: Don Kendall <dkendall@ledoweb.com>

* test(account): cover merge persons authorization

Nineteen cases over both operations: the reported attack shape, the
maintainer to owner escalation through an unverified email, foreign
accounts on either side of the merge, the platform guest account, and the
paths that must keep working (workspace contacts, members, tool service
and admin tokens). Removing the authorization check fails eight of them.

Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7
Signed-off-by: Don Kendall <dkendall@ledoweb.com>

---------

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
2026-07-27 11:46:18 +07:00
Why_So_SeriousandGitHub e34f546b8c fix(github): restore the recovery path for revoked user OAuth tokens (#10995)
* fix(github): honour the force flag in checkRefreshToken

`checkRefreshToken` accepts a `force` parameter but never reads it, so the
only caller that passes `force = true` (worker.ts, when re-syncing a user)
behaves exactly like the non-forced call and skips the refresh entirely.

Gate the refresh on `force || expired` instead of on expiry alone. As a
side effect a record with a `refreshToken` but a null `expiresIn` is now
validated when forced, instead of being reported as valid unchecked.

Signed-off-by: koreanjoker <namug014@gmail.com>

* fix(github): return undefined from getOctokit when the token is dead

When `checkRefreshToken` reports failure, `getOctokit` cleared
`record.octokit` and then fell straight through to constructing a new
Octokit from the very token that was just rejected. The method therefore
never returned `undefined` for a revoked user, so the
`(await getOctokit(...)) ?? container.container.octokit` installation-token
fallback that the sync code already writes at 13 call sites was
unreachable.

Return `undefined` after clearing the client so the existing fallback can
take effect.

Signed-off-by: koreanjoker <namug014@gmail.com>

* fix(github): preserve accounts when deserialising a user secret

`updateUser` serialises the whole `GithubUserRecord` — `accounts`
included — into the integration secret, but `secretToUserRecord` placed a
literal `accounts: {}` after the spread of the parsed payload, discarding
whatever was stored.

Every consumer of a record loaded through `getAccount` therefore saw an
empty map. `revokeUserAuth` iterates `Object.entries(record.accounts)`, so
its body never ran and the re-authorisation notice was never written to
any workspace.

Read `accounts` back from the parsed payload, keeping `{}` as the fallback
for records written before the field existed.

Signed-off-by: koreanjoker <namug014@gmail.com>

---------

Signed-off-by: koreanjoker <namug014@gmail.com>
2026-07-27 00:11:38 +07:00
Denis BykhovandGitHub c36dd74fef Add AllMatchValue function and related updates across multiple files (#10990)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
2026-07-21 20:51:02 +05:00
Artyom SavchenkoandGitHub 92eaefcc4d Fix workspace check (#10988)
* Fix workspace check

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Fix workspace validation

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-20 19:40:24 +07:00
Artyom SavchenkoandGitHub 5ca6b42529 Add script to validate workspace (#10987)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-20 12:03:45 +05:00
Denis BykhovandGitHub 4c94335cc1 Fix relation attribute editor (#10986)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
2026-07-20 11:42:27 +07:00
Artyom SavchenkoandGitHub c19694b476 Fix validation error in export service (#10985)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-19 21:29:16 +07:00
Artyom SavchenkoGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
3cae972cf5 Fix export sync endpoint in case of exporting several spaces (#10984)
* Fix export sync endpoint in case of exporting several spaces

Signed-off-by: Artyom Savchenko <armisav@gmail.com>

* Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Artyom Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artyom Savchenko <armisav@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-19 19:58:25 +07:00
Alexander OnnikovandGitHub 4c5d2d578e fix: use -webkit-user-select for text selection on Safari (#10982)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
2026-07-17 16:44:09 +07:00
Alexander OnnikovandGitHub 21dfcf0774 fix: allow inactive members in departments (#10983)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
2026-07-17 16:43:14 +07:00
Alexander OnnikovandGitHub 293bc91888 fix: proper hr members update (#10977)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
2026-07-14 17:28:54 +07:00
Artyom SavchenkoandGitHub ca2010fb63 Create SECURITY.md (#10971)
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
2026-07-14 15:09:29 +07:00