Commit graph

303 commits

Author SHA1 Message Date
Johannes Millan
602b8002e7 fix(sync-server): harden deploy with Caddyfile validation and container checks
- Pin Caddy image to 2.11-alpine to prevent breaking changes from
  floating tags
- Validate Caddyfile syntax before deploying to catch config errors
  early
- Check all container states after startup to detect crashes before
  waiting on the HTTPS health check
- Show logs from all services on failure, not just supersync
2026-03-23 13:34:05 +01:00
Johannes Millan
db30f92e23 fix(sync-server): remove invalid timeout subdirective from Caddyfile
Caddy v2 reverse_proxy does not support a top-level `timeout`
subdirective. The response_header_timeout in the transport block
already covers long-running operations.
2026-03-23 13:05:40 +01:00
Johannes Millan
c53c8bc38c fix(sync-server): remove no-new-privileges from Caddy container
Caddy Alpine images set file capabilities on the binary via setcap.
The no-new-privileges security option prevents the kernel from honoring
file capabilities during exec, causing "operation not permitted" on
startup. Container remains hardened with cap_drop: ALL + cap_add:
NET_BIND_SERVICE.
2026-03-23 13:03:54 +01:00
Johannes Millan
1eaa95c4fe fix(sync-server): add NET_BIND_SERVICE capability for Caddy container
Newer Caddy Alpine images set file capabilities on the binary, which
requires NET_BIND_SERVICE to bind to ports 80/443. Without it,
cap_drop: ALL prevents execution entirely.
2026-03-23 13:03:54 +01:00
Johannes Millan
5aff8d0aae fix(monitoring): use Prisma.sql for conditional SQL fragments in analyze-storage
Prisma's $queryRaw tagged template treats ${} as parameterized values, not
raw SQL. Conditional fragments like `${userId ? \`WHERE ...\` : ''}` were
sent as string parameters, causing PostgreSQL syntax error 42601.
2026-03-23 13:03:31 +01:00
Johannes Millan
2d7656c5c8 fix(monitoring): mask PII, add types, fix sort and validation issues
Mask user emails by default (--unmask to reveal). Replace all any[]
query results with typed interfaces. Fix showUsageHistory sort that
compared formatted byte strings. Add parseIntArg validation for CLI
flags. Fix unbounded recursion in analyzePayload. Replace execSync with
execFileSync for DATA_DIR to prevent shell injection. Add --no-save flag
for usage command.
2026-03-23 10:52:57 +01:00
Johannes Millan
fe65d81635
Add active users monitoring command with engagement metrics (#6921)
* feat(sync): add active-users command to monitor CLI

Adds a new `active-users` command to the SuperSync monitor script that reports:
- Total registered and verified user counts
- Active users by time period (24h, 7d, 30d, 90d) based on device and sync activity
- New registration counts
- Recently active users table with device count and ops
- Users who never synced

Usage: npm run monitor -- active-users

https://claude.ai/code/session_014Tc5vtXW4Z8QZFMDFWKErP

* feat(sync): add engaged users metric to active-users report

Shows users who were active on 3+ distinct days in the last 2 weeks
with new sync operations, giving a measure of genuine recurring usage.

https://claude.ai/code/session_014Tc5vtXW4Z8QZFMDFWKErP

* docs(sync): add active-users to docker monitoring docs

Add missing active-users command to docker-monitor.sh case statement,
help text, and DOCKER-MONITORING.md guide.

https://claude.ai/code/session_014Tc5vtXW4Z8QZFMDFWKErP

* refactor(sync): improve active-users command from review feedback

- Fix timezone-unsafe DATE(): use AT TIME ZONE 'UTC' with explicit
  double precision cast for consistent day boundaries
- Replace correlated subquery with LEFT JOIN for ops_7d count
- Show total active count when LIMIT truncates the table
- Add --threshold and --limit CLI flags for flexibility
- Combine device/ops metrics into single line (connected / syncing)
- Add skipInQuick to run-all-monitoring.ts
- Update docker-monitor.sh header, help text, and DOCKER-MONITORING.md
  with active-users command, flags, and performance notes
- Clarify "never synced" output label

https://claude.ai/code/session_014Tc5vtXW4Z8QZFMDFWKErP

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 23:15:14 +01:00
Johannes Millan
43e04bc407 fix(sync): fix supersync server unit test failures
Add pretest script to run prisma generate before tests, ensuring the
Prisma client is always generated in any environment. Fix verifyToken
mocks across all test files to include valid:true matching the
TokenVerificationResult discriminated union. Expand jsonwebtoken mock
to include JsonWebTokenError/TokenExpiredError stubs needed by auth.ts.
Add $queryRaw to transaction mock in sync-fixes tests.
2026-03-22 10:10:19 +01:00
Johannes Millan
f26732755f fix(sync): prevent transient server errors from wiping auth credentials
Server: verifyToken() catch-all was returning 401 "Invalid token" for ALL
errors including database failures. Now only JWT-specific errors
(JsonWebTokenError, TokenExpiredError) return 401. Database/infrastructure
errors propagate as 500s with sanitized logging.

Client: SuperSync AuthFailSPError no longer clears stored credentials —
the server rejection may be transient. The snackbar with "Configure"
button still shows so users can re-auth on genuine failures. Dropbox and
WebDAV still clear immediately so their OAuth/re-auth flows work.
2026-03-20 21:36:30 +01:00
Johannes Millan
2cc2760486 feat(i18n): complete translations and update locale files
Translate all missing keys to achieve full coverage (2086/2086)
for fr, pt-br, zh-tw, ro, ro-md, zh, es, de, and cs.
Also remove stale keys and include updated translations
for other locales. Remaining languages still have partial coverage.
2026-03-18 21:55:13 +01:00
Johannes Millan
7c0849e953 fix(sync-server): auto-pull latest code in deploy script
Adds git pull --ff-only before docker compose pull so scripts,
docker-compose.yml, and other repo files are updated on each deploy.
Falls back gracefully if pull fails.
2026-03-18 20:51:59 +01:00
Johannes Millan
1907df68d7 fix(sync-server): security hardening and GDPR log compliance
- Remove email addresses from all log messages (~18 locations) for GDPR
  compliance, replacing with userId where available
- Downgrade debug credential logs from info to debug level
- Remove options.user from passkey registration log (contained email in
  JSON payload)
- Add container security hardening to docker-compose.yml:
  - no-new-privileges on all containers
  - cap_drop: ALL on supersync and caddy
  - Memory limits (supersync 512m, postgres 1g, caddy 256m)
  - CPU limit on supersync (1.0)
- Add .health-alert/ to gitignore
- Add health-alert.sh cron script for container monitoring with
  OOM detection, disk checks, and email alerting
2026-03-18 20:48:51 +01:00
Johannes Millan
a01a511c25 feat(sync): add backup strategy with accounts-only dump and disaster recovery docs
- Add accounts-only pg_dump (users + passkeys) to backup script for
  lightweight disaster recovery when clients still have data
- Add pipefail to backup script to catch silent dump failures
- Add test endpoint to simulate partial server revert (ops-after/:serverSeq)
- Add 6 e2e tests covering all disaster recovery scenarios:
  complete data loss, partial revert, accounts-only restore (API + SQL),
  full dump restore + reset account, and all-clients-lost recovery
- Add backup-and-recovery.md with setup, recovery procedures, and
  decision tree
2026-03-18 20:15:45 +01:00
Johannes Millan
609d6a7217 chore(sync): exclude integration test from default run, scope integration config 2026-03-17 13:59:40 +01:00
Johannes Millan
861425fd28 refactor(sync): improve vector clock implementation quality
- Replace O(N) Prisma findMany with PostgreSQL SQL aggregate
  (jsonb_each_text + GROUP BY + MAX) for snapshot vector clock
  computation, avoiding loading all ops' clocks into Node.js memory
- Add defensive regex filter (^[0-9]+$) for non-numeric JSONB values
- Fix || to ?? for nullish coalescing in vector clock value lookups
- Remove unreachable CONCURRENT check in compareVectorClocks
- Remove unused VectorClockMetrics interface and measureVectorClock
- Add 7 new unit tests for SQL aggregate path verification
- Add 10 integration tests running actual SQL against PostgreSQL
2026-03-17 13:59:40 +01:00
Johannes Millan
d401e6169e feat(tasks): add deadlines feature with reminders and planner integration
Add deadline support for tasks with date-only and time-specific deadlines.

Core:
- Add deadlineDay, deadlineWithTime, deadlineRemindAt fields to task model
- Add NgRx actions (setDeadline, removeDeadline, clearDeadlineReminder)
- Add meta-reducer with mutual exclusivity and input validation
- Register deadline actions in ActionType enum and op-log codes

UI:
- Create deadline dialog with calendar, time input, and reminder options
- Add deadline badge to task list row with overdue warning color
- Add deadline item to task detail panel with flag icon
- Add deadline options to task context menu
- Show deadlines in scheduled list page

Reminders:
- Add deadline reminder effects and selectors
- Show reminder dialog with grouped deadline/schedule sections
- Handle deadline reminder clearing on dismiss, done, add-to-today
- Cancel Android native deadline notifications on delete/archive

Planner:
- Show deadline tasks in planner day view and overdue section
- Create dedicated planner-deadline-task component
- Optimize deadline grouping with O(N) single-pass selector

Other:
- Add deadline-today banner effect for unplanned deadline tasks
- Add translation keys for all deadline UI strings
- Add E2E tests for deadline reminder flows
- Add unit tests for deadline reducer and overdue utility
- Extract shared isDeadlineOverdue utility function
- Guard app-state selectors against undefined during teardown
2026-03-15 18:48:00 +01:00
Johannes Millan
baf6124415 fix: address code review findings from yesterday's changes
- Add error handler to markdown paste promise in add-task-bar
- Fix misleading "try again later" in resend cap error message
  (cap is permanent, not time-based)
2026-03-13 11:07:22 +01:00
Johannes Millan
b5536adfe5 feat(sync): add verification resend cap of 20 attempts
Prevents email bombing by capping verificationResendCount at 20 for
both magic link and passkey registration flows. Returns a safe error
message when the cap is reached.
2026-03-12 13:50:50 +01:00
Johannes Millan
3ba5841421 feat(sync): add magic link registration to SuperSync server
Add email-only registration flow (no passkey required) with verification
email. For existing unverified users, email is sent before DB token
update to preserve the old verification link on failure.

- POST /api/register/magic-link endpoint with Zod validation and rate limiting
- registerWithMagicLink function in auth.ts with P2002 race condition handling
- Frontend "Register with Email" button and JS handler
- Export shared VERIFICATION_TOKEN_EXPIRY_MS constant (eliminates duplication)
- 8 unit tests covering all paths including email failure cleanup
2026-03-12 13:36:05 +01:00
dependabot[bot]
620cd3dd52
chore(deps): bump fastify in the npm_and_yarn group across 1 directory (#6753)
Bumps the npm_and_yarn group with 1 update in the / directory: [fastify](https://github.com/fastify/fastify).


Updates `fastify` from 5.7.3 to 5.8.1
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](https://github.com/fastify/fastify/compare/v5.7.3...v5.8.1)

---
updated-dependencies:
- dependency-name: fastify
  dependency-version: 5.8.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 11:02:30 +01:00
Johannes Millan
cce9576946 test(e2e): fix supersync E2E tests for mandatory encryption flow
- Extract shared encryption warning helper and fix race conditions
- Restructure concurrent-import tests to import before enabling sync
- Fix encryption password preservation and change tests
- Add "Change Password" button detection in supersync page object
- Mark backup-import ID mismatch test as fixme (known limitation)
2026-03-06 16:36:38 +01:00
Johannes Millan
3dc86dd183
Fix/add rate limits (#6745)
* fix(build): fix tag resolution and add error handling in bump-android-version

The previous two-tag approach (`currentTag` → `prevTag`) would resolve the
wrong changelog range during `npm version` since the new tag doesn't exist
yet. Simplified to a single `lastTag...HEAD` range which correctly captures
all commits since the last release.

Added try-catch with fallback to last 20 commits when no tags exist, and
a fallback message for empty changelogs.

* fix(sync): differentiate auth error messages and clarify token revocation

- Rename server dashboard "Refresh Token" to "Revoke & Replace Token"
  with explicit warning that ALL devices will be disconnected
- Return rejection reason in 401 responses (revoked, expired, etc.)
  via discriminated union TokenVerificationResult type
- Show different client error messages for server-side token rejection
  vs missing local credentials to aid user diagnosis
- Extract server error reason from JSON response body in AuthFailSPError

Closes #6597

* refactor(sync): use generic auth reason and improve test readability

- Replace 'User not found' and 'Account not verified' with generic
  'Account unavailable' to avoid leaking account state in API responses
- Extract long reason strings to constants in middleware.spec.ts

* fix(sync-server): add per-route rate limits to silence CodeQL alert

Add explicit rate limits to routes that only had the global rate limit:
- GET /api/sync/status: 60/min
- DELETE /api/sync/data: 3/15min (destructive operation)
- GET /api/sync/restore-points: 30/min
- GET /api/sync/restore/:serverSeq: 10/5min (CPU-intensive)
- GET /reset-password: 20/15min
- GET /recover-passkey: 10/15min

These complement the global @fastify/rate-limit (100/15min) and silence
the CodeQL 'Missing rate limiting' alert on authenticated handlers.
2026-03-05 21:18:36 +01:00
Johannes Millan
7070fde51e
Pr 6741 (#6743)
* fix(build): fix tag resolution and add error handling in bump-android-version

The previous two-tag approach (`currentTag` → `prevTag`) would resolve the
wrong changelog range during `npm version` since the new tag doesn't exist
yet. Simplified to a single `lastTag...HEAD` range which correctly captures
all commits since the last release.

Added try-catch with fallback to last 20 commits when no tags exist, and
a fallback message for empty changelogs.

* fix(sync): differentiate auth error messages and clarify token revocation

- Rename server dashboard "Refresh Token" to "Revoke & Replace Token"
  with explicit warning that ALL devices will be disconnected
- Return rejection reason in 401 responses (revoked, expired, etc.)
  via discriminated union TokenVerificationResult type
- Show different client error messages for server-side token rejection
  vs missing local credentials to aid user diagnosis
- Extract server error reason from JSON response body in AuthFailSPError

Closes #6597

* refactor(sync): use generic auth reason and improve test readability

- Replace 'User not found' and 'Account not verified' with generic
  'Account unavailable' to avoid leaking account state in API responses
- Extract long reason strings to constants in middleware.spec.ts
2026-03-05 21:17:59 +01:00
Johannes Millan
da8e509972 refactor(sync): unify JWT expiry to 365 days for all auth methods
Replace separate JWT_EXPIRY_MAGIC_LINK (365d) and JWT_EXPIRY_PASSKEY (7d)
constants with a single JWT_EXPIRY (365d). The auth method only matters
during login — once a JWT is issued, it represents a verified session
regardless of how the user authenticated.
2026-02-16 18:44:27 +01:00
Johannes Millan
2bc73607ff
Add snapshotOpType parameter for correct operation typing (#6482)
* fix(sync): pass correct opType from client to server in snapshot upload

The server was hardcoding opType as 'SYNC_IMPORT' for all snapshot
uploads, losing the distinction between SYNC_IMPORT, BACKUP_IMPORT,
and REPAIR operations. Now the client sends snapshotOpType in the
request body, and the server uses it (with fallback to 'SYNC_IMPORT'
for backward compatibility with legacy clients).

https://claude.ai/code/session_01XnqdpGbFR6VRPdCAbZ2EB9

* test(sync): fix uploadSnapshot test expectations for 9th snapshotOpType arg

The uploadSnapshot call now passes snapshotOpType as a 9th argument.
Update all 5 failing test expectations to include the new argument
(SYNC_IMPORT, BACKUP_IMPORT, or REPAIR) and fix the argument count
check from 8 to 9.

https://claude.ai/code/session_01XnqdpGbFR6VRPdCAbZ2EB9

* fix(sync): add missing snapshotOpType param to file-based sync adapter

The file-based adapter's uploadSnapshot signature was missing the 9th
snapshotOpType parameter added in f328c35, making it inconsistent with
the provider interface. Add the parameter (unused, as file-based sync
has no server-side op log).

https://claude.ai/code/session_01XnqdpGbFR6VRPdCAbZ2EB9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 16:06:39 +01:00
Johannes Millan
454fefcb04
test(sync): add vector clock pruning edge case tests (#6506)
Add tests for previously untested vector clock pruning scenarios:

- Multi-preserve-ID pruning (two low-counter IDs both preserved)
- Overlapping/duplicate preserve IDs (Set deduplication)
- Preserve IDs missing from clock (silent skip, no crash)
- Snapshot vector clock aggregation + pruning integration
- Post-snapshot comparison correctness after pruning
- Server sanitizeVectorClock counter cap at 100M
- DoS cap at 2.5x MAX (reject, not prune)
- Invalid inputs (null, arrays, empty keys, long keys, negative/float values)

https://claude.ai/code/session_01GdsbKoo8eax394j2UyWUu1

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 12:42:36 +01:00
Johannes Millan
534f375a54 fix(sync): clear sync cycle cache on deleteAllData and extract ConflictType
Clear _syncCycleCache in _deleteAllData() to prevent stale cached data
from being served within the 30s TTL after a data wipe. Extract
ConflictType and ConflictResult into named types in sync.types.ts to
eliminate duplicated inline unions in sync.service.ts.
2026-02-12 16:27:59 +01:00
Johannes Millan
43b5808a3f fix(sync): harden vector clock sanitization and improve E2E test robustness
- Cap sanitizeVectorClock values at 100M instead of MAX_SAFE_INTEGER (prevents adversarial clock inflation)
- Add early-exit optimization in compareVectorClocks for CONCURRENT detection
- Add test.setTimeout(180000) to token-expiry and lastseq-preservation E2E tests
- Remove dead code (unreachable guard) in compaction E2E test
- Extract shared config helpers (navigateToMiscSettings, toggleSetting, etc.) into e2e/utils/config-helpers.ts
- Fix Client B cleanup in provider-switch test (move to finally block)
- Add clarifying comments for raw SQL schema coupling, CORS header, and mergeRemoteOpClocks caller contract
- Extract enforceStorageQuota helper in sync routes to reduce duplication
- Add conflictType to conflict detection for structured error handling
- Add MAX_CLEANUP_ITERATIONS guard to freeStorageForUpload
2026-02-12 16:27:56 +01:00
Johannes Millan
ab0d979e5e fix(sync): revert lastSeq preservation in deleteAllUserData
Unlike uploadOps clean-slate (which must preserve lastSeq to prevent
sequence reuse for other clients), deleteAllUserData should reset
lastSeq to 0. Account reset intentionally wipes everything and clients
detect the wipe via latestSeq=0, triggering a full state re-upload.
2026-02-12 16:27:56 +01:00
Johannes Millan
5b7c9bdfb7 test(sync): add comprehensive SuperSync test coverage
Add 7 E2E tests (lastSeq preservation, token expiry recovery, cascade
delete, compaction resilience, global config edge cases, provider switch,
multi-tab), 2 server unit tests (replaceToken expiry regression,
middleware auth), 2 client integration tests (IndexedDB error recovery,
schema version sync), and locale-independent sort edge cases for vector
clocks.
2026-02-12 16:27:56 +01:00
Johannes Millan
c70ced204e fix(sync): fix stale comments, doc refs, and preserve lastSeq on clean-slate
Remove references to deleted docs/ai/ files, update stale comments about
protectedClientIds and pruning-aware comparison to reflect current REPLACE
semantics, fix MAX=30→20 heading, client_0..29→19 comment, use ?? over ||,
preserve lastSeq in server clean-slate path to prevent sequence reuse, and
add clarifying comments for mock limitations and validation guards.
2026-02-12 16:27:56 +01:00
Johannes Millan
3f99e3773c fix(sync): fix replaceToken expiry, locale-independent sort, and stale comments
- Use JWT_EXPIRY_PASSKEY (7d) for replaceToken instead of 365d magic link
  expiry — token replacement is a security action, shorter lifetime is safer
- Replace localeCompare with locale-independent comparator in vector clock
  tie-breaking to ensure deterministic behavior across environments
- Fix 5 additional stale MAX=30 references in docs and tests (now 20)
- Update authentication.md to reflect dual JWT expiry tiers
- Clean up isLikelyPruningArtifact references in docs and LEGACY_MAX in tests
2026-02-12 16:27:55 +01:00
Johannes Millan
ef8ad4ded7 refactor(sync): address code review findings across server and client
- Consolidate getJwtSecret() and JWT_EXPIRY constants into auth.ts,
  eliminating 3x duplication with inconsistent validation (C1+C2)
- Add isVerified defense-in-depth check to verifyToken() (C3)
- Add Content-Transfer-Encoding to CORS allowedHeaders for
  Android/Capacitor clients (C4)
- Fix 9 stale comments referencing MAX_VECTOR_CLOCK_SIZE as 30
  when actual value is 20 (I1)
- Remove dead _syncVectorClockFromPfapi method and update
  referencing comments (S8)
- Replace in-memory payload size calculation with SQL aggregate
  in deleteOldestRestorePointAndOps to avoid loading large
  payloads into Node memory (I7)
2026-02-12 16:27:55 +01:00
Johannes Millan
0b73461690 fix(sync): fix vector clock comparison parity and validation consistency
- Fix client/server comparison divergence: remove isVectorClockEmpty
  short-circuit that caused {} vs {a:0} to return LESS_THAN on client
  but EQUAL on shared impl. Now delegates all comparisons to shared.
- Align server value cap with client: raise from 10M to MAX_SAFE_INTEGER
  to prevent silent clock entry stripping on long-term usage.
- Make client validation require integers (Number.isInteger) matching
  server-side validation.
- Add deterministic tie-breaking (localeCompare) to pruning sort for
  equal counter values.
- Fix stale comments: MAX references and CLAUDE.md DoS cap (5x→2.5x).
2026-02-12 16:27:55 +01:00
Johannes Millan
a335589dc6 refactor(sync): reduce vector clock DoS cap from 100 to 50 entries 2026-02-12 16:27:55 +01:00
Johannes Millan
f37280e082 refactor(sync): reduce MAX_VECTOR_CLOCK_SIZE from 30 to 20
Lower the cap to leave headroom for future increases and surface
size-related edge cases earlier. All pruning logic is MAX-agnostic
so this is a safe constant change with documentation updates.
2026-02-12 16:27:55 +01:00
Johannes Millan
77972bfe80 fix(sync): fix stale comments and string concatenation bug from vector clock simplification
Fix NaN in diagnostic log (unary + on string), update DoS cap comment
(50→150), remove dead normalization ternary, and update stale
"pruning-aware" references in comments.
2026-02-12 16:27:55 +01:00
Johannes Millan
29541951a3 refactor(sync): increase MAX_VECTOR_CLOCK_SIZE from 10 to 30 and remove defense layers
At MAX=10, pruning triggered frequently enough (11+ unique client IDs from
reinstalls/new browsers) to require 4 defense layers compensating for
information loss: pruning-aware comparison, protected client IDs with
migration, isLikelyPruningArtifact heuristic, and same-client check.

At MAX=30, pruning almost never triggers (needs 31+ unique client IDs).
A 30-entry clock is ~500 bytes — negligible bandwidth. This allows removing
most defense layers while keeping two cheap backward-compat checks for old
10-entry pruned data still on servers.

Removed:
- Pruning-aware mode in compareVectorClocks (standard comparison now)
- Protected client IDs mechanism (storage, migration, preservation)
- selectProtectedClientIds function
- Clock normalization in SyncImportFilterService

Kept temporarily (backward compat with old 10-entry data):
- isLikelyPruningArtifact with LEGACY_MAX=10
- Same-client check (always mathematically correct)
2026-02-12 16:27:55 +01:00
Johannes Millan
fc56aabec4 docs(sync): fix stale DoS cap references from 3x to 5x MAX_VECTOR_CLOCK_SIZE
The sanitizeVectorClock() DoS cap was changed to 5x MAX (50 entries) but
comments in CLAUDE.md, vector-clocks.md, sync.types.ts, and
validation.service.ts still referenced the old 3x MAX (30) value.
2026-02-10 20:38:32 +01:00
Johannes Millan
d4cd9a3575 fix: address 9 code review findings
- Change server-side pruning log from info to debug
- Increase vector clock sanitize limit from 3x to 5x MAX
- Add Formly hierarchy comment for WebDAV settings
- Add test documenting isLikelyPruningArtifact false positive limitation
- Replace `as any` with proper Partial<TaskRepeatCfgCopy> typing
- Add comment explaining `order` in SCHEDULE_AFFECTING_FIELDS
- Cache Intl.Collator instance, recreate only on locale change
- Document plugin management 30s timeout rationale
- Extract _isLikelyPruningArtifact as standalone pure function
2026-02-09 17:55:12 +01:00
Johannes Millan
a774c46700 fix(sync): remove client-side vector clock pruning from ConflictResolutionService and fix rejection counting
- Remove limitVectorClockSize from ConflictResolutionService (same
  infinite loop risk as SupersededOperationResolverService)
- Include retry-exceeded ops in permanentRejectionCount so sync status
  accurately reflects data loss
- Fix stale JSDoc (100 -> 30 entries) in sanitizeVectorClock
- Remove dead getProtectedClientIds mocks from spec files
- Extract _getEntityKey helper, remove redundant guards, optimize
  Object.keys calls in server pruning log
2026-02-09 17:55:12 +01:00
Johannes Millan
fdc942babb
fix(sync): prevent infinite loop when concurrent modification resolution keeps failing (#6434)
* fix(sync): prevent infinite loop when concurrent modification resolution keeps failing

When vector clock pruning makes it impossible to create a dominating clock
(e.g., entity clock has MAX entries and client ID isn't among them), the cycle
"upload → CONFLICT_CONCURRENT → merge clocks → upload → reject again" repeats
endlessly. This adds a per-entity retry counter (MAX_CONCURRENT_RESOLUTION_ATTEMPTS=3)
that permanently rejects ops after exceeding the limit, breaking the sync loop.

The counter resets when a sync cycle completes with no rejections (healthy state).

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* fix(sync): move vector clock pruning after conflict detection to fix root cause

The infinite sync loop happens because it's mathematically impossible to build
a dominating clock with MAX_VECTOR_CLOCK_SIZE entries when the entity's clock
already has MAX entries and the client's ID isn't among them. The merged clock
needs MAX+1 entries (all entity clock IDs + client ID), but client-side pruning
drops one entity clock ID. The server's pruning-aware comparison then sees the
dropped key as non-shared and returns CONCURRENT instead of GREATER_THAN.

Fix: Move limitVectorClockSize from validation (before comparison) to
processOperation (after comparison, before storage). The full unpruned clock
is now used for conflict detection — all entity clock IDs are present so
bOnlyCount=0 → GREATER_THAN. Storage still gets the pruned clock.

Client-side: Stop pruning in SupersededOperationResolverService. The server
handles pruning after conflict detection.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* fix(sync): tighten vector clock sanitize limit from 100 to 3x MAX_VECTOR_CLOCK_SIZE

The old sanitize cap of 100 entries was unnecessarily wide. Since conflict
resolution clocks are at most ~12-15 entries (entity clock MAX=10 + client ID
+ a few merged), cap at 3x MAX (30) for DoS protection while leaving ample
room for legitimate clocks.

Also update server-side pruning tests to use realistic clock sizes (20 entries
instead of 50) to stay within the new sanitize limit.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* docs(sync): document vector clock pruning invariant and infinite loop fix

- Add "Pruning and the Pruning-Aware Comparison" section to vector-clocks.md
  explaining the critical invariant: server prunes AFTER comparison, not before
- Add rule 13 to CLAUDE.md to prevent future regressions
- Update conflict resolution key files table with rejected-ops-handler and
  superseded-operation-resolver services

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* docs(sync): update last-updated date in conflict resolution docs

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* test(sync): update pruning tests to reflect server-side pruning design

Client no longer prunes vector clocks during conflict resolution — the
server handles pruning after conflict detection. Tests now verify the
merged clock is sent unpruned with all keys preserved.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* test(sync): fix client-side pruning tests and add server-side regression test

- Update 7 SupersededOperationResolverService tests to verify client
  does NOT prune (server handles pruning after conflict detection)
- Remove redundant `newClock` alias in superseded-operation-resolver
- Add regression test: MAX+1 entry clock accepted as GREATER_THAN
  when it dominates a MAX entry entity clock (the core infinite loop fix)

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-09 12:01:43 +01:00
dependabot[bot]
518d40f6c1
chore(deps): bump fastify in the npm_and_yarn group across 1 directory (#6334)
Bumps the npm_and_yarn group with 1 update in the / directory: [fastify](https://github.com/fastify/fastify).


Updates `fastify` from 5.6.2 to 5.7.3
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](https://github.com/fastify/fastify/compare/v5.6.2...v5.7.3)

---
updated-dependencies:
- dependency-name: fastify
  dependency-version: 5.7.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 12:19:34 +01:00
Johannes Millan
850f7248a7 chore(sync): extend JWT expiry to 365 days 2026-02-02 17:15:13 +01:00
Johannes Millan
cd2ae61ae0 Revert "feat(sync): add rolling JWT token refresh on sync requests"
This reverts commit eca323505d1923fcfc470d78fc8984251e825382.
2026-02-02 17:15:13 +01:00
Johannes Millan
717e4ba84d feat(sync): add rolling JWT token refresh on sync requests
On each successful sync request, the server now returns a fresh 14-day
JWT via X-Refreshed-Token response header. The client reads and stores
it automatically, preventing token expiry during active usage.

Server: export JWT_EXPIRY/getJwtSecret from auth, add createRefreshedToken(),
add onSend hook scoped to sync routes, expose header via CORS.
Client: store refreshed token in all 4 fetch methods, fix _getServerSeqKey
to hash only baseUrl (prevents full re-download on token change).
2026-02-02 17:15:13 +01:00
tandy1000
551319293a
Update README to require PUBLIC_URL for configuration (#6300)
Mark PUBLIC_URL as required in the README.

I tried to run this locally and I got an error when I didn't define this in the .env, but no error for not defining the SMTP server which definitely seems to be needed.
2026-02-02 14:21:55 +01:00
Johannes Millan
c5409bbd25 fix(sync): add server pruning log, harden test coverage for vector clocks
- Add Logger.warn() in ValidationService when oversized vector clocks
  are pruned server-side, improving observability for buggy clients
- Add tests for hasVectorClockChanges (previously zero coverage)
- Add symmetric LESS_THAN test for pruning-aware comparison mode
- Export MAX_LWW_REUPLOAD_RETRIES constant to eliminate magic number
  coupling between sync-wrapper service and its test
- Add test verifying uploading client ID is preserved during server-side
  clock pruning
- Add test for limitVectorClockSize when preserveClientIds exceeds MAX
2026-01-30 19:50:30 +01:00
Johannes Millan
495f0fe0d3 fix(sync): improve vector clock pruning awareness and harden tests
Make hasVectorClockChanges pruning-aware by checking clock size before
logging missing keys — downgrade to verbose when pruning is likely,
warn when corruption is likely. Add return value assertion to LWW retry
exhaustion test, asymmetric pruning test cases, server-side pruning
tradeoff documentation, and fix stale "max 50" in docs.
2026-01-30 19:35:51 +01:00
Johannes Millan
b113a2d7dd fix(sync): return CONCURRENT instead of EQUAL when pruned clocks have non-shared keys
When both vector clocks are at MAX size (pruning-aware mode) and shared
keys compare as equal, non-shared keys on both sides indicate genuinely
different causal histories. Returning EQUAL caused silent data loss by
skipping operations as duplicates. Returning CONCURRENT safely triggers
LWW conflict resolution instead.

Also fixes server snapshot pruning to preserve the requesting client's
ID and the snapshot author's client ID when limiting vector clock size.
2026-01-30 19:21:45 +01:00