- B7: replace leaky `Transaction rolled back: <raw Prisma msg>` per-op
error with a generic 'Transaction failed - please retry'. The raw
exception text is still logged via `Logger.error`, but never returned
to the client (could leak SQL fragments, FK / column names).
- B6: in `deleteOldestRestorePointAndOps`, measure the exact bytes for
the deleted full-state op (SYNC_IMPORT / BACKUP_IMPORT / REPAIR) row
via `pg_column_size` BEFORE deletion. Bounded to 0-1 rows per call,
so it doesn't reintroduce the DoS from scanning every delta op. Delta
ops keep the cheap `count * APPROX_BYTES_PER_OP` approximation;
full-state payloads (up to 20MB) no longer undercount by ~20000x and
leave the counter permanently low if reconcile fails. Docstring on
APPROX_BYTES_PER_OP clarifies it is ONLY valid for delta cleanup.
- I6: inside `deleteOldSyncedOpsForAllUsers`, call
`storageQuotaService.markNeedsReconcile(userId)` for each user whose
cleanup deleted rows. A crash mid-loop now leaves surviving users
with a forced-reconcile marker so their next request self-heals,
instead of waiting for the daily pass. TODO left for persisting the
marker in a DB column for cross-restart / multi-instance safety.
- B3 (service side): write `users.storage_used_bytes` atomically
inside the upload `$transaction` using `$executeRaw` with the same
GREATEST(...) clamp pattern as `storage-quota.service.ts`. Shared
`computeOpStorageBytes` helper keeps the gate and the increment in
agreement about per-op size. Clean-slate path SETs the counter to
the new total rather than incrementing onto the just-reset row.
- B2 companion: add `RequestDeduplicationService.clearForUser(userId)`
and call it from both `deleteAllUserData` and the uploadOps
clean-slate path so a retry from before the wipe cannot return
cached results that reference now-deleted state.
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.
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.
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
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).
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.
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.
Fixes identified issues in the operation log sync system:
1. [High] Upload deduplication now returns fresh piggybacked ops on retry
- Deduplicated responses now use the retry request's lastKnownServerSeq
- Ensures clients don't miss operations from other clients
2. [High] Server conflict detection now checks all entityIds in batch ops
- Multi-entity operations (entityIds array) now properly checked
- Each entity ID is verified for concurrent modifications
3. [High] Encrypted snapshot uploads now properly flagged
- Added isPayloadEncrypted parameter to uploadSnapshot interface
- Server stores and returns the flag with operations
- Enables proper decryption on download
4. [Medium] clientId validation added to server
- Rejects operations where op.clientId doesn't match request clientId
- Returns INVALID_CLIENT_ID error code
5. [Low] Error classification now uses structured errorCode
- Client checks errorCode instead of string matching error messages
- More reliable conflict detection
Also adds test infrastructure for Prisma-based server tests:
- New vitest.config.ts with proper setup
- Test setup file with Prisma mocks
- sync-fixes.spec.ts with tests for issues 1 and 3
Note: Legacy SQLite-based tests are excluded pending migration to
async Prisma patterns. Client tests all pass (133/133).