Commit graph

134 commits

Author SHA1 Message Date
Johannes Millan
cbeecfd39a fix(sync): multi-tab vector clock staleness + shared code extraction
Bug fix:
- Fix vector clock cache staleness in multi-tab scenarios by clearing
  cache when acquiring operation write lock. Each browser tab has its
  own in-memory cache, so Tab B's cache could be stale if Tab A wrote
  while Tab B was waiting for the lock.

Shared code extraction (client/server consistency):
- Extract vector clock comparison to @sp/shared-schema
  - Client wraps shared impl with null handling
  - Server imports directly from shared
- Extract entity types to @sp/shared-schema
  - Single source of truth for ENTITY_TYPES array
  - Removes duplicated "must match" comments

Files:
- packages/shared-schema/src/vector-clock.ts (new)
- packages/shared-schema/src/entity-types.ts (new)
- src/app/op-log/store/operation-log-store.service.ts (cache clear)
- src/app/op-log/capture/operation-log.effects.ts (call cache clear)
2026-01-03 18:05:11 +01:00
Johannes Millan
a57a197d44 fix(sync-server): fix double encoding of passkey credential ID
SimpleWebAuthn's credentialInfo.id is a Uint8Array containing the
base64url-encoded credential ID as UTF-8 bytes, NOT the raw credential
ID bytes. We were storing these ASCII bytes directly, then encoding
them again as base64url during lookup.

During login, the browser sends the original base64url string, which
we decoded to raw bytes - but this didn't match the double-encoded
value stored in the database.

The fix decodes the base64url string from credentialInfo.id before
storing:
1. Convert Uint8Array to UTF-8 string (the base64url string)
2. Decode base64url to get raw credential ID bytes
3. Store raw bytes in database

Now login correctly looks up by raw credential ID bytes.
2026-01-03 13:32:37 +01:00
Johannes Millan
c07d0a4588 fix(sync-server): look up passkey by credential ID instead of email
With discoverable credentials, the user can select any passkey for
this RP. We need to look up the passkey by the credential ID that
comes back from the browser, not by the email the user entered.

This fixes the 401 error when the user's passkey credential ID
doesn't match what we expected based on email lookup.
2026-01-03 13:20:34 +01:00
Johannes Millan
0f8d2cbc42 chore: code review improvements for operation-logs branch
Phase 1 - Code Quality:
- Add clarifying comment in lww-update.meta-reducer.ts explaining
  Date.now() choice for LWW conflict resolution
- Document multi-instance deployment limitations in README.md
  (passkey challenges, snapshot locks)

Phase 2 - Testing:
- Add TODAY_TAG selector edge case tests (deleted/archived tasks)

Phase 3 - Minor Polish:
- Add createValidationErrorResponse() helper to hide Zod validation
  details in production (sync.routes.ts)
- Add database index @@index([userId, opType]) for faster restore
  point queries
2026-01-03 13:17:42 +01:00
Johannes Millan
374ba9873a fix(sync-server): use discoverable credentials for passkey login
Instead of providing allowCredentials (which requires the browser to
match specific credential IDs), let the browser discover all resident
credentials for this RP automatically.

This matches how passkeys.io works and should fix the "no passkeys
available" issue on mobile.
2026-01-03 13:16:01 +01:00
Johannes Millan
f1eab1d8ca debug(sync-server): add detailed credential ID logging for passkey
Log the full credential ID (both hex and base64url) during:
- Registration: to see exactly what's being stored
- Login: to see exactly what's being retrieved from DB

This will help debug why passkeys aren't being found during login.
2026-01-03 13:01:34 +01:00
Johannes Millan
d5e196b82b fix(sync-server): use v13 API format in passkey recovery page
The recovery page was using the old SimpleWebAuthn API format:
  startRegistration(options)

Updated to use the v13 format:
  startRegistration({ optionsJSON: options })

This was causing "nothing happens" when clicking Register New Passkey.
2026-01-03 12:14:47 +01:00
Johannes Millan
3a7191d5f5 fix(sync-server): require discoverable credentials for passkeys
Change residentKey from 'preferred' to 'required' for passkey registration.
Discoverable credentials (resident keys) are required for synced passkeys
like Google Password Manager to properly store and retrieve the passkey.

Also add detailed logging of registration options for debugging.
2026-01-02 22:07:03 +01:00
Johannes Millan
4cee57db14 fix(sync-server): fix SimpleWebAuthn v13 type imports
Import types from @simplewebauthn/server instead of deprecated
@simplewebauthn/types package which is no longer supported in v13.
2026-01-02 22:00:10 +01:00
Johannes Millan
f143210cd9 fix(sync-server): update SimpleWebAuthn to v13 and add debug logging
- Update @simplewebauthn/server from v11 to v13.2.2 for compatibility
- Add debug logging for WebAuthn config and authentication options
- Browser library also needs to be updated to v13.2.2 (manual step)

The version mismatch between server and browser libraries may have been
causing passkey registration/login issues.
2026-01-02 21:58:49 +01:00
Johannes Millan
4ca44fd3d3 add logs 2 2026-01-02 21:23:35 +01:00
Johannes Millan
92b8aa8178 add logs 2026-01-02 21:16:03 +01:00
Johannes Millan
f09d7c51b4 fix(sync-server): allow passkeys without user verification
The @simplewebauthn/server library defaults to requireUserVerification: true,
but our registration options set userVerification: 'preferred'. This mismatch
caused passkey registration and login to fail with:
"User verification was required, but user could not be verified"

Add requireUserVerification: false to all verification calls to match
the 'preferred' setting in registration/authentication options.
2026-01-02 21:07:37 +01:00
Johannes Millan
2ec5e08df4 fix(sync): address code review findings for operation log
- Add startup warning in passkey.ts when running with in-memory
  challenge storage in production (multi-instance deployments)
- Use LOCK_NAMES.OPERATION_LOG constant in stale-operation-resolver
  instead of hardcoded 'sp_op_log' string
- Add client-side validation for empty/whitespace entityId to match
  server-side validation and provide earlier feedback
2026-01-02 18:13:22 +01:00
Johannes Millan
3a179df3cc fix(sync): prevent data loss when multiple clients join SuperSync
Fixes critical bug where multiple clients with existing data could each
create a SYNC_IMPORT when enabling SuperSync, causing data loss.

Root cause: When clients had local data (from legacy migration), each
would detect "server needs migration" and create competing SYNC_IMPORTs.
The last one uploaded would invalidate all prior data.

Fixes:
- Server rejects duplicate SYNC_IMPORT with SYNC_IMPORT_EXISTS (409)
- Client double-checks server is empty before creating SYNC_IMPORT
- Client merges all local op clocks into SYNC_IMPORT's vector clock
- hasSyncedOps() excludes MIGRATION/RECOVERY ops from check
- Upload service gracefully handles SYNC_IMPORT_EXISTS rejection

Tests added:
- ServerMigrationService: double-check and clock merging (5 tests)
- OperationLogStoreService: hasSyncedOps() MIGRATION exclusion (7 tests)
- OperationLogUploadService: SYNC_IMPORT_EXISTS handling (5 tests)
- E2E: Multiple clients with existing data merge correctly
2026-01-02 17:51:12 +01:00
Johannes Millan
9c0a728ef4 feat(sync-server): replace password auth with passkey + magic link
Authentication changes:
- Add passkey (WebAuthn) as primary login method
- Add email magic link as fallback for devices without passkey support
- Remove password-based authentication entirely

New features:
- Passkey registration and login via @simplewebauthn/server
- Magic link login with 15-minute expiry tokens
- Passkey recovery via email link
- Self-hosted simplewebauthn-browser.min.js for reliability

Database changes:
- Add Passkey model for WebAuthn credentials
- Add PasskeyChallenge model for registration/auth challenges
- Add loginToken and loginTokenExpiresAt fields for magic links
- Add passkeyRecoveryToken fields for passkey recovery

UI changes:
- Login form: email + "Login with Passkey" + "Send Login Link"
- Register form: email + terms checkbox + "Register with Passkey"
- Consistent token display UI for both passkey and magic link login
- Remove password fields and forgot password flow

Security:
- CSP-compliant magic link redirect using external script
- Rate limiting on all auth endpoints
- Single-use magic link tokens
2026-01-02 16:52:48 +01:00
Johannes Millan
4f2dbcdaa7 feat(sync): handle auth errors for account deletion scenarios
When a SuperSync account is deleted, clients now properly detect
authentication failures and prompt for reconfiguration:

- Add _checkHttpStatus() helper to SuperSyncProvider that throws
  AuthFailSPError on 401/403 responses
- Clear cached credentials on auth failure to allow reconfiguration
- Add DELETE /api/test/user/:userId endpoint for E2E testing
- Add deleteTestUser() helper in supersync-helpers.ts
- Add E2E tests for account deletion and reconfiguration scenarios

The existing SyncWrapperService already handles AuthFailSPError by
showing a snackbar with "Configure" action, so no UI changes needed.
2026-01-02 15:48:17 +01:00
Johannes Millan
4e0f5e8999 fix(e2e): improve app loading detection and fix server TypeScript errors
- e2e/utils/waits.ts: Add proper retry logic and error handling when
  waiting for magic-side-nav to appear. Now throws a clear error if
  app doesn't load within 30s instead of silently continuing.

- packages/super-sync-server/src/auth.ts: Use nullish coalescing for
  passwordHash to handle passkey-only users (who have no password).

- packages/super-sync-server/src/passkey.ts: Add WebAuthn passkey
  authentication support. Fix TypeScript errors by importing types
  from @simplewebauthn/types and converting credential IDs to
  base64url strings.

Fixes flaky "Multiple fresh clients join and sync correctly after
snapshot" test that was failing when app didn't fully load.
2026-01-02 15:11:16 +01:00
Johannes Millan
187cbdafe3 fix(sync-server): reject empty/whitespace-only entityId in validation
The server was accepting empty string entityId values because the validation
only checked type and length, not content. This could allow corrupt operations
to be synced to other clients.

Changes:
- Add check for empty/whitespace-only entityId (returns INVALID_ENTITY_ID)
- Add 3 unit tests for empty, whitespace-only, and tab/newline entityId
- Update existing empty string test to expect INVALID_ENTITY_ID (format error)
  instead of MISSING_ENTITY_ID (null/undefined check)
2026-01-02 13:58:07 +01:00
Johannes Millan
b367c9595b feat(sync): add account deletion to SuperSync web frontend
Add ability for users to delete their SuperSync account from the
web frontend. This addresses GitHub issue #5848.

Server changes:
- Add DELETE /api/account endpoint in api.ts
- Requires JWT authentication
- Uses Prisma cascade delete for user and related data
- Rate limited to 3 requests per 15 minutes
- Logs USER_ACCOUNT_DELETED audit event

Web frontend changes:
- Add "Delete Account" button with danger styling
- Confirmation dialog warns about permanent deletion
- Clears state and returns to login on success
2026-01-02 13:25:40 +01:00
Johannes Millan
2a9b84caf0 fix(api): add missing await in replace-token endpoint
The /replace-token endpoint was calling replaceToken() without await,
causing the response to return an unresolved Promise instead of the
actual token. This is the same class of bug as the email verification
race condition fixed in fd6499f13.

Also adds unit tests for the setCounter() fix from issue #5812:
- Tests for creating new counters with all mandatory fields
- Tests for updating existing counters (only countOnDay)
- Tests for incrementCounter and decrementCounter behavior
- Tests for error handling (invalid keys, negative values)
2026-01-01 12:24:32 +01:00
Johannes Millan
c087ffee8a fix(sync): prevent infinite rejection loop from corrupt operations
Root cause: Operations with `entityId: undefined` (corrupt ops) caused
an infinite rejection loop during sync:
1. Upload ops → Rejected with CONFLICT_CONCURRENT
2. Rejection handler creates merged ops (still with invalid entityId)
3. Upload merged ops → Rejected again
4. Loop continues forever

This prevented the sync status double checkmark from ever appearing
because hasPendingOps never became false.

Three-layer defense implemented:

1. Client: Guard time tracking dispatch (task.service.ts)
   - Changed `if (currentTask &&` to `if (currentTask?.id &&`
   - Prevents time tracking dispatch with undefined taskId

2. Client: Strengthen entityId validation (operation-log.effects.ts)
   - Check that entityId is truthy AND a string type
   - Log error with actual entityId value for debugging

3. Client: Cleanup corrupt ops on startup (operation-log-recovery.service.ts)
   - New cleanupCorruptOps() method marks corrupt ops as rejected
   - Runs during hydration to break existing infinite loops
   - Excludes bulk 'ALL' operations which legitimately lack entityId

4. Server: Validate entityId for all regular operations (validation.service.ts)
   - Reject ops with missing entityId (except full-state and bulk ops)
   - Full-state: SYNC_IMPORT, BACKUP_IMPORT, REPAIR
   - Bulk entity types: ALL, RECOVERY
   - Returns MISSING_ENTITY_ID error code (permanent rejection)
2025-12-31 17:29:25 +01:00
Johannes Millan
a86df2c356 fix(sync): prevent duplicate operation from aborting batch upload
When a duplicate operation was uploaded, PostgreSQL's unique constraint
violation (P2002) would abort the entire transaction, causing all
subsequent operations in the batch to fail with INTERNAL_ERROR.

Server-side fix:
- Add pre-check via findUnique before attempting insert
- Return DUPLICATE_OPERATION error code without aborting transaction
- Other ops in batch continue processing normally

Client-side fix:
- Handle DUPLICATE_OPERATION error code in rejected-ops-handler
- Mark duplicate as synced (not rejected) since server already has it
- Prevents infinite retry loop for legitimately uploaded ops

Adds tests for both server and client handling.
2025-12-31 16:39:55 +01:00
Johannes Millan
fd6499f138 fix(security): address critical and high severity vulnerabilities
Security fixes implemented:

1. CRITICAL: Fix missing await in email verification (pages.ts:139)
   - verifyEmail() was called without await, causing race condition
   - Response was sent before verification completed

2. CRITICAL: Fix XSS vulnerability in password reset page (pages.ts)
   - Added safeJsonForScript() to properly escape tokens in JS context
   - JSON.stringify alone doesn't escape </script> sequences
   - Now escapes <, >, & as unicode (\u003c, \u003e, \u0026)

3. CRITICAL: Fix XSS in privacy HTML template (server.ts)
   - Added escapeHtml() function for all template interpolations
   - Prevents XSS if environment variables contain malicious content

4. HIGH: Enable Content Security Policy (server.ts)
   - CSP was disabled (contentSecurityPolicy: false)
   - Now enabled with strict directives:
     - default-src 'self', object-src 'none', frame-ancestors 'none'

5. HIGH: Block wildcard CORS in production (config.ts)
   - CORS_ORIGINS=* with credentials is a security vulnerability
   - Now throws error in production, warns in development

6. HIGH: Add password reset flow (auth.ts, api.ts, email.ts, schema.prisma)
   - Secure token generation with crypto.randomBytes(32)
   - 1-hour expiry, one-time use tokens
   - Revokes all sessions on password reset
   - Prevents email enumeration (same response for all cases)
   - Rate limited: 5 requests/15min for forgot-password
   - Rate limited: 10 requests/15min for reset-password

Test coverage:
- 45 new tests across 4 test files
- Tests for XSS prevention, CORS blocking, CSP headers
- Tests for password reset flow (API and unit level)
- Fixed pre-existing flaky boundary test in retention-config.spec.ts
2025-12-31 12:15:56 +01:00
Johannes Millan
c9a25a988d perf(sync): skip download when all ops fit in piggyback response
Optimize sync to reduce round-trips when remote changes are small:

- Increase PIGGYBACK_LIMIT from 100 to 500 ops on server
- Skip download call when server confirms hasMorePiggyback === false
- CRITICAL: Only skip when hasMorePiggyback is explicitly false, not undefined
  (undefined means no upload happened, so we must still download)

This reduces sync from 2 round-trips to 1 when ≤500 ops are pending,
improving perceived sync performance.

Add 5 unit tests for the skip-download optimization logic.
2025-12-29 21:52:03 +01:00
Johannes Millan
61b8d82ad6 fix(sync): address security and reliability issues from code review
- Add payload complexity validation for full-state ops (SYNC_IMPORT,
  BACKUP_IMPORT, REPAIR) with higher thresholds (50 depth, 100k keys)
  to prevent DoS attacks via deeply nested payloads
- Make persistence failure notification sticky (duration: 0) so users
  don't miss critical errors that require page reload
- Fix misleading comment in gap detection (effectiveSinceSeq, not sinceSeq)
- Add comprehensive unit tests for StaleOperationResolverService (16 tests)
  covering vector clock merging, timestamp preservation, and edge cases
2025-12-29 12:03:49 +01:00
Johannes Millan
1da70487f9 fix: address code review findings from Dec 27-29 changes
ESLint rule improvements:
- Fix require-hydration-guard to detect guards on combineLatest/forkJoin/zip
  results (eliminates 20+ false positive warnings)

Server improvements:
- Add OperationDownloadService unit tests (21 tests covering vector clock
  aggregation, gap detection, excludeClient parameter, snapshot optimization)
- Split ZIP bomb size limits: 10MB for /ops, 30MB for /snapshot
- Document storage quota update as intentionally non-atomic

E2E test improvements:
- Add waitForUISettle() helper using Angular stability instead of fixed timeouts
- Update supersync-cross-entity tests to use dynamic waits

Unit test improvements:
- Add HydrationStateService edge case tests (concurrent calls, cooldown cycles,
  timer cleanup, interleaved operations)
2025-12-29 10:37:12 +01:00
Johannes Millan
f780099ef8 fix(sync): address code review findings from 2025-12-28 changes
- Add unit tests for extracted hydrator services (60 new tests):
  - operation-log-recovery.service.spec.ts (17 tests)
  - operation-log-snapshot.service.spec.ts (20 tests)
  - sync-hydration.service.spec.ts (23 tests)

- Add compressed size pre-check in sync.routes.ts:
  - New MAX_COMPRESSED_SIZE (10MB) to prevent memory exhaustion
  - Two-stage protection: pre-check compressed, post-check decompressed
  - Applied to both /ops and /snapshot upload endpoints

- Remove tombstone references from sync-server-architecture-diagrams.md:
  - Database schema, ER diagram, cleanup tasks

- Replace console.warn with Logger.warn in sync.types.ts
2025-12-28 16:56:56 +01:00
Johannes Millan
4d5c79f129 feat(supersync): add privacy policy address via env vars
For German legal compliance (Impressum), the privacy policy needs
to display the operator's address. This change:

- Rename privacy.html to privacy.template.html with placeholders
- Add PRIVACY_* env vars to .env.example
- Generate privacy.html at server startup from template
- Add PrivacyConfig to ServerConfig

If env vars are not set, placeholder text is shown.
2025-12-28 15:38:40 +01:00
Johannes Millan
ed061f90fb fix(sync): clamp future timestamps instead of rejecting
Operations with timestamps beyond maxClockDriftMs (60s) were being
permanently rejected, causing silent data loss when a client's clock
was slightly ahead.

Now these timestamps are clamped to now + maxClockDriftMs and accepted.
A TIMESTAMP_CLAMPED audit log is recorded for debugging.

- Add clamping logic in SyncService.processOperation()
- Remove future timestamp rejection from ValidationService
- Add comprehensive tests verifying clamped values and boundaries
- Update integration test to expect acceptance
2025-12-28 12:23:36 +01:00
Johannes Millan
17b94f0644 docs: add cross-reference comments for vector clock comparison
Add SYNC NOTE comments to both client and server compareVectorClocks
implementations pointing to each other. This prevents accidental drift
between the two implementations which must use identical algorithms.
2025-12-28 12:17:30 +01:00
Johannes Millan
a52b7716aa refactor(sync): remove tombstone system
Tombstones were used for tracking deleted entities but are no longer
needed with the operation log architecture. This removes:

- Tombstone table from Prisma schema
- Tombstone-related methods from SyncService
- Tombstone mocks and tests from all test files
- Database migration to drop tombstones table

The operation log now handles deletions through DEL operations,
making the separate tombstone tracking redundant.
2025-12-28 12:08:28 +01:00
Johannes Millan
805327731b refactor(sync): unify retention config to single value
- Replace opRetentionMs, maxOpAgeMs, and STALE_DEVICE_THRESHOLD_MS
  with single retentionMs (45 days)
- Consolidate 5 cleanup jobs into 1 daily job
- Remove tombstone-related code and tests (tombstones were removed
  in a previous migration)
- Simplifies configuration and removes contradictory values
  (maxOpAgeMs was 30 days while opRetentionMs was 45 days)
2025-12-28 12:05:44 +01:00
Johannes Millan
9e8d05f430 test(sync): add error handling tests for SuperSync
Add comprehensive tests for error code handling at unit and E2E levels:

Unit tests (super-sync.spec.ts):
- 429 Rate Limited response handling
- 413 Storage Quota Exceeded response handling
- Parse CONFLICT_CONCURRENT/STALE/DUPLICATE_OPERATION from response
- Mixed accept/reject batch categorization
- Extract piggybacked ops from conflict response

Unit tests (operation-log-sync.service.spec.ts):
- STORAGE_QUOTA_EXCEEDED shows alert but doesn't mark op rejected
- INTERNAL_ERROR marks op as rejected
- Verify subsequent operations behavior after errors

E2E tests (supersync-error-handling.spec.ts):
- Concurrent modification triggers LWW conflict resolution
- Sync recovers after initial connection
- Duplicate sync attempts handled gracefully
- Three clients converge to same state

Also adds SnapshotService.clearForUser() helper for test cleanup.
2025-12-28 11:49:21 +01:00
Johannes Millan
77a4ca34cb refactor(sync): remove unused parentOpId field
Remove the parentOpId field from the operation log system. It was
designed for conflict resolution chain tracking but was never
implemented - the field was always undefined/null.

Client changes:
- Remove from Operation interface
- Remove from compact codec encoding/decoding
- Update tests

Server changes:
- Remove from Operation type and Zod validation
- Remove from persistence and download mapping
- Add database migration to drop the column
2025-12-28 11:48:38 +01:00
Johannes Millan
fc05a00514 refactor(sync): remove redundant 5-minute snapshot TTL cache
The snapshot route had a time-based TTL (5 minutes) that could return
stale snapshots even when fresher data was available. This was redundant
because generateSnapshot() already handles caching via sequence-based
freshness - it returns the cached snapshot if up-to-date and only
replays ops when new ones exist.

Remove snapshotCacheTtlMs from SyncConfig and simplify the GET /snapshot
route to just call generateSnapshot() directly.
2025-12-28 10:48:00 +01:00
Johannes Millan
578faafb0b fix(sync): handle base64-encoded gzip from Android clients
Android WebView's fetch() corrupts binary Uint8Array bodies, causing
"incorrect header check" errors when decompressing gzip data on the
server. This fix uses CapacitorHttp with base64-encoded gzip on Android.

Client changes:
- Add isAndroidWebView getter for testability
- Add _fetchApiCompressedAndroid() method using CapacitorHttp
- Modify uploadOps() and uploadSnapshot() to use Android path
- Send Content-Transfer-Encoding: base64 header to signal encoding

Server changes:
- Add decompressBody() helper to handle base64+gzip
- Check Content-Transfer-Encoding header in /ops and /snapshot routes
- Decode base64 before gunzip when header is present

Net bandwidth savings ~50% (gzip saves 70%, base64 adds 33% overhead).

Includes comprehensive tests for both client and server paths.
2025-12-28 10:37:45 +01:00
Johannes Millan
18f03c1ec7 refactor(sync): extract SnapshotService from SyncService
Extract snapshot-related functionality into dedicated SnapshotService:
- getCachedSnapshot, cacheSnapshot, generateSnapshot
- getRestorePoints, generateSnapshotAtSeq, replayOpsToState

Key changes:
- Preserve FIX 1.7 concurrent snapshot lock behavior
- Fix SYNC_IMPORT/BACKUP_IMPORT/REPAIR handling to run before
  entity type check (these are full-state operations)
- Add comprehensive unit tests for SnapshotService

SyncService reduced from ~1988 to 918 lines (54% reduction).
2025-12-27 20:59:31 +01:00
Johannes Millan
4f3f778da5 refactor(sync): extract StorageQuotaService from SyncService
Extract storage quota calculation and checking methods:
- calculateStorageUsage(): Calculate total storage from ops + snapshot
- checkStorageQuota(): Check if upload is within quota
- updateStorageUsage(): Update cached storage counter
- getStorageInfo(): Get storage stats for status endpoint

Complex cleanup operations (freeStorageForUpload, deleteOldestRestorePointAndOps)
remain in SyncService as they orchestrate multiple operations.

All 246 tests passing.
2025-12-27 20:48:08 +01:00
Johannes Millan
07589dd67f refactor(sync): extract DeviceService and OperationDownloadService
Continue SyncService extraction with two more services:

- DeviceService: device ownership and online status queries
  (isDeviceOwner, getAllUserIds, getOnlineDeviceCount)
- OperationDownloadService: operation retrieval with gap detection
  and snapshot optimization (getOpsSince, getOpsSinceWithSeq, getLatestSeq)

SyncService now delegates to 6 extracted services:
- ValidationService
- RateLimitService
- RequestDeduplicationService
- DeviceService
- OperationDownloadService

All 236 tests passing.
2025-12-27 20:43:25 +01:00
Johannes Millan
2a9e3154bc refactor(sync): extract services from SyncService and migrate tests to Prisma
Extract three services from the monolithic SyncService for better
separation of concerns and testability:

- ValidationService: validates operations (validateOp, validatePayloadComplexity)
- RateLimitService: handles rate limiting with in-memory counters
- RequestDeduplicationService: handles request deduplication cache

Also migrate legacy tests from synchronous SQLite patterns to async
Prisma mocks:
- Create shared test state module (sync.service.test-state.ts)
- Migrate conflict-detection.spec.ts, gap-detection.spec.ts,
  sync-operations.spec.ts to Prisma mocks
- Add comprehensive unit tests for extracted services

Test count: 227 passing (up from 164 before service extraction)
2025-12-27 20:38:26 +01:00
Johannes Millan
9ee15b9f08 fix(sync): iteratively cleanup storage until quota satisfied
When storage quota is exceeded, the server now iteratively deletes
old restore points and operations until enough space is freed,
keeping at least one restore point as minimum valid sync state.

Changes:
- Add freeStorageForUpload() method that loops until quota satisfied
- Update deleteOldestRestorePointAndOps() to clear stale snapshot cache
- Update /ops and /snapshot routes to use iterative cleanup
- Add comprehensive tests for new cleanup behavior

The error response now includes cleanupStats showing what was freed
even when cleanup ultimately fails due to minimum data requirements.
2025-12-25 10:18:35 +01:00
Johannes Millan
8f277adc60 fix(sync): prevent concurrent uploads causing transaction rollback
Root cause: On Android, LockService was skipping locks entirely, allowing
ImmediateUploadService and main sync to upload the same operations
concurrently. This caused PostgreSQL serialization failures under
REPEATABLE_READ isolation.

Fixes:
1. LockService: Use fallback mutex on Electron/Android instead of no
   locking. These are single-instance but still need in-process locking
   for concurrent code paths.

2. Server: Add detection for serialization failures (Prisma P2034,
   PostgreSQL 40001/40P01). These are logged as warnings with clearer
   error messages for retry.

Combined with the previous commit that treats INTERNAL_ERROR as
transient (not permanent rejection), these errors will now be
automatically retried on next sync.
2025-12-24 16:43:03 +01:00
Johannes Millan
0b2a394f2e feat(sync): add storage quota auto-cleanup and user alerts
When server storage quota is exceeded:

1. Server-side auto-cleanup:
   - Recalculate actual storage before failing (fixes stale cache issue)
   - Delete oldest restore point and all ops before it to free space
   - If only one restore point, keep it but delete ops before it
   - Retry quota check after cleanup before returning error

2. Client-side strong warnings:
   - Show window.alert when STORAGE_QUOTA_EXCEEDED error received
   - Alert on both ops upload and snapshot upload failures
   - Makes sync failure immediately visible to users

3. Improved error messaging in translations

Includes comprehensive unit tests for all cleanup scenarios.
2025-12-24 16:17:48 +01:00
Johannes Millan
72608a4aaa feat(migration): implement pre-migration dialog and backup functionality 2025-12-24 13:49:37 +01:00
Johannes Millan
1bd96a9182 fix(sync): refresh storage cache after cleanup and add serverTime to response
Address two issues identified in code review:

1. Storage cache not refreshed after cleanup:
   - Modified deleteOldSyncedOpsForAllUsers() to return { totalDeleted, affectedUserIds }
   - Cleanup job now calls updateStorageUsage() for each affected user
   - Prevents stale quota checks after nightly cleanup

2. serverTime missing from download response:
   - Added serverTime: Date.now() to DownloadOpsResponse
   - Enables client-side clock drift detection

Tests:
- Added serverTime response test in sync-fixes.spec.ts
- Added deleteOldSyncedOpsForAllUsers return structure tests
- Updated legacy SQLite tests for new return type (excluded from CI)
2025-12-23 18:31:57 +01:00
Johannes Millan
a405b6ee6d fix: address code review issues from sync and UX changes
Critical fixes:
- fix(sync): handle empty newOps with hasMorePiggyback flag
- fix(sync): handle skippedOps (stale operations) in sync service
- fix(tasks): replace requestAnimationFrame with RxJS delay(0)

High priority fixes:
- fix(sync): add rollback recovery for password change failures
- fix(e2e): improve password change verification robustness
- fix(sync): add try-catch to WebDAV test connection
- fix(sync): improve encrypted restore error message with guidance
- fix(android): add startWith() to fix pairwise() initial state

Medium priority fixes:
- fix(sync): add diagnostics to flush timeout error
- docs: document focus delay constants and rationale
- docs: add TODO for Android reminder native action handlers
2025-12-22 21:09:18 +01:00
Johannes Millan
064c2452ca fix(sync): handle piggyback limit in high-volume sync
When syncing 100+ operations, the server's piggyback limit (100 ops)
was causing Client B to miss operations. The server returned 100 piggybacked
ops but latestSeq was set to the actual server sequence (e.g., 199).
Client B then updated lastServerSeq to 199, so subsequent download got 0 ops.

Changes:
- Server: Add hasMorePiggyback flag to UploadOpsResponse when piggyback limit
  is reached and more ops exist
- Client: When hasMorePiggyback is true, store lastServerSeq as the max
  piggybacked op's serverSeq instead of latestSeq, ensuring subsequent
  download fetches remaining ops
- Effects: Change switchMap to mergeMap in autoAddTodayTagOnMarkAsDone to
  ensure ALL mark-as-done actions trigger planTasksForToday
- Flush service: Implement two-phase wait strategy (poll queue + acquire lock)
  to ensure all pending writes complete before upload
- Add diagnostic logging for operation counts at key stages

Test: High volume sync with 50 tasks + 49 mark-as-done (197 ops total)
now correctly syncs all done states via piggyback (100) + download (97).
2025-12-22 20:31:35 +01:00
Johannes Millan
c4cc32da29 feat(sync): add encryption password change feature for SuperSync
Implements the ability to change the encryption password by deleting
all server data and uploading a fresh snapshot with the new password.

Server changes:
- Add DELETE /api/sync/data endpoint to delete all user sync data
- Add deleteAllUserData() method to SyncService

Client changes:
- Add deleteAllData() to OperationSyncCapable interface
- Implement deleteAllData() in SuperSync provider
- Add EncryptionPasswordChangeService to orchestrate password change
- Add DialogChangeEncryptionPasswordComponent with validation
- Add "Change Encryption Password" button to sync settings (visible
  when encryption is enabled)
- Add translations for all new UI strings

Testing:
- Add 10 unit tests for EncryptionPasswordChangeService
- Add 14 unit tests for DialogChangeEncryptionPasswordComponent
- Add 5 E2E tests for complete password change flow
- Add changeEncryptionPassword() helper to SuperSyncPage

Also fixes:
- Add missing deleteAllData() to MockOperationSyncProvider
- Fix typo S_FINISH_DAY_SYNC_ERROR -> FINISH_DAY_SYNC_ERROR
2025-12-22 13:31:19 +01:00
Johannes Millan
291c424210 fix(sync): address 5 critical sync system issues
1. CRITICAL: Encrypted ops blocking snapshot/restore
   - Server now rejects restore requests when encrypted ops exist
   - Added ENCRYPTED_OPS_NOT_SUPPORTED error code
   - replayOpsToState defensively skips encrypted ops

2. HIGH: Dedup before quota check
   - Moved request deduplication BEFORE quota check
   - Ensures retries after successful uploads don't fail with 413

3. MEDIUM: Chunked uploads reusing lastKnownServerSeq
   - Now updates lastKnownServerSeq between chunks
   - Prevents duplicate piggybacked ops on subsequent chunks

4. MEDIUM: Vector-clock EQUAL from different client
   - EQUAL clocks from different clients now treated as conflict
   - Added explicit handling for all comparison cases
   - Removed unsafe catch-all that returned hasConflict:false

5. LOW: SuperSync status localStorage scoping
   - Added setScope/clearScope methods to SuperSyncStatusService
   - Storage key now includes scope ID (hash of server+token)
   - Prevents stale status when switching accounts/servers

Tests: Added unit tests for vector clock comparison and updated
all relevant test files with comprehensive coverage.
2025-12-22 10:32:57 +01:00