Add a trust badge displaying EU flag stars and "Data hosted in EU" text to the SuperSync server login/register page. The badge uses official EU flag colors (blue #039 and gold #fc0) with the circle of 12 stars from public domain SVG.
Internal compliance documentation has been moved to a private location.
These documents contain sensitive operational procedures and security
analysis that should not be public.
Files moved:
- GDPR compliance analysis
- Incident response playbooks
- Data subject request procedures
- DPIA screening decisions
- Records of processing activities
- Infrastructure verification documents
Update incident response, data subject request, and DPIA procedures to
accurately reflect that database encryption at rest is NOT implemented
for non-E2EE users.
Changes:
- INCIDENT-RESPONSE-PLAYBOOK.md: Clarify E2EE is optional throughout,
add physical server compromise scenarios, update risk assessments to
differentiate E2EE vs non-E2EE users, document encryption gap in
prevention measures
- DATA-SUBJECT-REQUEST-PROCEDURES.md: Add encryption status disclosure
to access responses, clarify data export formats, add security notice
about unencrypted storage for non-E2EE users
- DPIA-SCREENING-DECISION.md: Document encryption gap as additional
consideration, update risk level to LOW-MEDIUM, add encryption gap
to conclusion and re-assessment triggers
All procedures now consistently acknowledge 85% compliance score and
risk variance based on E2EE usage, while maintaining that DPIA is not
required per Art. 35.
Update GDPR compliance documentation to accurately reflect that database
encryption at rest is NOT implemented for non-E2EE users. This critical
finding required:
- Update compliance score from 92% to 85% (10% deduction for encryption gap)
- Add comprehensive encryption disclosure to privacy policies (German & English)
- Document risk: unencrypted PostgreSQL data on disk
- Update GDPR analysis with compensating controls (optional E2EE)
- Revise Records of Processing Activities with encryption status
- Add context to Alfahosting verification tracker
Changes prioritize GDPR transparency by honestly documenting security
limitations rather than overstating compliance.
- Remove placeholder address text from privacy policies (DE/EN)
- Expand HTML privacy policy with full GDPR disclosures:
- Legal bases (Art. 6), data subject rights (Art. 15-22)
- Supervisory authority, retention periods, DPA info
- Cookies/tracking and automated decision-making sections
- Align HTML terms with German ToS:
- Add proper termination notice periods (2 weeks/good cause)
- Add 6-week notice for ToS amendments
- Add consumer withdrawal rights (14 days)
- Add ODR platform link and jurisdiction info
When uploading BACKUP_IMPORT via uploadSnapshot(), the client's op.id
was not sent to the server. The server would generate its own ID,
causing the client to not recognize the operation when downloaded later.
This led to data loss as the old backup state would be re-applied.
Changes:
- Add opId parameter to uploadSnapshot() interface
- Pass op.id from operation-log-upload.service.ts
- Send opId in SuperSync API request payload
- Server uses client's opId instead of generating new one
- Add E2E test to verify ID matching
The fix is backwards compatible - legacy clients without opId still work
as the server falls back to uuidv7() when opId is not provided.
Add explicit per-route rate limits to /verify-email and /magic-login
endpoints to address CodeQL security alerts. These endpoints previously
relied only on global rate limiting.
npm 10.9.4 bundled with node:22-alpine handles optional peer
dependencies differently than npm 11, causing build failures
when resolving vitest peer dep from @angular-devkit/build-angular.
- Fix TypeScript error in passkey.ts (Buffer to Uint8Array conversion)
- Update Dockerfile.test to use npm install instead of npm ci for workspace sync
- Update E2E test to not setup sync before import
- Add additional unit tests for sync-wrapper.service
Note: E2E tests for sync import conflict dialog need further work due to
automatic upload of SYNC_IMPORT when sync is enabled. The core dialog
functionality is implemented and unit tested.
When all remote operations are filtered due to a local SYNC_IMPORT
(e.g., user changed sync account), show a dialog offering three options:
- Use My Data: Push local state to server (forceUploadLocalState)
- Use Server Data: Accept server state (forceDownloadRemoteState)
- Cancel: Abort sync for now
Changes:
- Extend SyncImportFilterService to return filteringImport operation
- Extend RemoteOpsProcessingService to return filter metadata
- Create DialogSyncImportConflictComponent with Material Dialog
- Create SyncImportConflictDialogService to open the dialog
- Integrate dialog trigger into OperationLogSyncService
- Add translation keys to en.json
- Add comprehensive unit tests (86 tests passing)
- Add E2E test for dialog flow
Also increases maxKeys threshold in super-sync-server validation
to 500K to handle archives with 300K+ keys.
Add automatic dark mode support using prefers-color-scheme media query:
- Dark backgrounds: #1a1a1a (page), #2d2d2d (cards)
- Light text: #e5e5e5 (primary), #a3a3a3 (secondary)
- Adjusted primary color for dark: #4db8e8 (7.74:1 contrast)
- Dark variants for warning/success/error message boxes
- Button backgrounds kept darker for white text contrast
Also add consistent link styling for Terms/Privacy links:
- Normal, visited, and hover states use primary color
- Works in both light and dark modes
All colors verified to meet WCAG AA 4.5:1 minimum contrast.
Update CSS color variables to meet WCAG AA 4.5:1 contrast ratio:
- --primary: #0c96e2 → #0077b6 (4.87:1 on white)
- --text-light: #666666 → #595959 (7.00:1 on white)
- --success: #4caf50 → #2e7d32 (5.13:1 white on bg)
- --error: #f44336 → #c62828 (5.62:1 white on bg)
- .warning-box: #856404 → #6d5200 (6.63:1 on #fff3cd)
All text elements now meet WCAG AA accessibility requirements.
Security:
- Add rate limiting (10 req/5min) to GET /snapshot endpoint
- Prevents DoS via CPU-intensive snapshot generation
Consistency:
- Add 30s timeout to download transaction (matches other sync transactions)
Test robustness:
- Fix weak encryption test - always assert error on wrong password
- Update lww-update.meta-reducer tests to use OpLog instead of console
Defensive coding:
- Add entity-not-found warnings in LWW meta-reducer for sync race conditions
- Log when project/tag/parent task deleted before LWW update arrives
Code quality:
- Standardize logging to OpLog in lww-update.meta-reducer
- Document LWW action types as intentionally not in ActionType enum
- Create e2e-constants.ts for centralized E2E test timeouts/delays
- Extract createProjectReliably helper to supersync-helpers.ts (DRY)
1. Add DELETE /api/sync/data route tests to sync.routes.spec.ts:
- Test successful deletion returns { success: true }
- Test 401 without authorization
- Test uploading new data works after reset
2. Fix passkey.spec.ts failures (4 tests):
- Add missing passkey.findUnique mock for credential lookup
- Update test expectations for discoverable credentials
(no allowCredentials - implementation changed)
3. Fix password-reset-api.spec.ts failures (12 tests):
- Exclude from vitest - tests routes that don't exist
- Server uses passkey/magic link auth, not password auth
All 412 tests now pass.
Add comprehensive unit tests for the reset account functionality:
- Test that deleteAllUserData removes all operations for a user
- Test that new operations can be uploaded after reset
- Test that resetting one user doesn't affect other users' data
Also adds missing userSyncState.deleteMany mock to the test setup.
Add a dedicated Account Settings page to the SuperSync server web interface:
- Move Reset Account and Delete Account buttons from token display to
separate Account Settings page for better UX
- Reset Account clears all synced data but keeps account active
- Delete Account permanently removes account and all data
- Add E2E tests for account reset functionality
Closes#5848
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)
- Email is now shared between login, register, and lost passkey forms
- Email persists to localStorage across page reloads
- Typing in any email field updates all others in real-time
Existing passkeys were stored with double-encoded credential IDs due to
a bug where SimpleWebAuthn's credentialInfo.id (UTF-8 bytes of base64url
string) was stored directly instead of being decoded to raw bytes first.
This migration script converts existing passkeys from the old format
(ASCII bytes of base64url string) to the correct format (raw credential
ID bytes).
Run with: npx ts-node prisma/migrations/migrate-passkey-credentials.ts
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.
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.
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.
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.
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.
The server was updated to @simplewebauthn/server v13.2.2 but the
browser library was still at pre-v13. The v13 API expects options
wrapped as { optionsJSON: options }, causing allowCredentials to be
lost during login with the old library.
This caused "no passkeys found" errors when users tried to login
after registering.
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.
- 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.
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.
The deleteAccount fetch request sent Content-Type: application/json
but no body, causing Fastify's JSON parser to fail with 400 Bad Request.
Add empty JSON body to match the pattern used by refreshToken.
- 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
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
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
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.
- 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.
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)
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