feat(pro): add micasa pro encrypted multi-device sync#791
Merged
Conversation
…to ULID strings
Replace auto-increment uint primary keys with 26-character ULID text strings
across all entity models as the foundation for multi-device sync. This is
Phase 1 of the micasa Pro migration.
Key changes:
- Add internal/uid package wrapping oklog/ulid/v2 for ULID generation
- Convert all 12 entity model PKs and FKs from uint to string with
BeforeCreate hooks that auto-assign ULIDs
- Fix all GORM queries: .First(model, stringID) -> .First(model, "id = ?", id)
and .Delete(model, stringID) -> .Where("id = ?", id).Delete(model) to
prevent SQL injection with string IDs
- Migrate FTS5 from content_rowid=id to content_rowid=rowid since FTS5
requires integer rowids
- Update shadow DB cross-referencing to use ordinal string IDs for batch
entity remapping
- Change zero-value sentinel from uint(0) to "" throughout app and data layers
- Update all ~60 store method signatures, handler interfaces, form data
structs, and view types
- Update vendorHash in flake.nix for new oklog/ulid/v2 dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full 26-char ULIDs are noisy in table views. Show only the last 7 characters (highest entropy / random component) while preserving the full ID in rowMeta for all lookups and operations. Cap column width at 7 to keep it compact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every data mutation (insert, update, soft-delete, restore) now produces an append-only oplog entry in sync_oplog_entries. AfterCreate hooks on all 11 syncable models capture inserts; updates, deletes, and restores use explicit oplog writes in Store methods since GORM's Model().Updates() and Where().Delete() pass empty models to hooks. Document payloads exclude BLOB data and include a blob_ref for content-addressed sync. A context flag (syncApplyingKey) suppresses oplog writes when applying remote operations to prevent infinite loops. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NaCl secretbox (XSalsa20-Poly1305) encryption with household symmetric keys and Curve25519 device keypairs for future key exchange. Keys are stored in $XDG_DATA_HOME/micasa/keys/ with 0600 permissions, separate from the SQLite database so backups remain data-only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relay server stores and routes encrypted sync operations between household devices without decrypting any data. Includes shared types in internal/sync, a Store interface with in-memory implementation for testing, HTTP handlers with bearer token auth, and comprehensive tests covering two-device sync, pagination, and monotonic sequence assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sync client encrypts local ops and pushes to the relay, pulls remote ops and decrypts them. Op applier uses last-write-wins per row with timestamp comparison and device_id tiebreaker. Conflict losers are preserved in the oplog with applied_at = NULL for audit and recovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…exchange
Implements Phase 6 of the sync system:
- NaCl box (Curve25519 + XSalsa20-Poly1305) for asymmetric key exchange
- Invite code generation (8-char base32, 24h expiry, max 5 attempts)
- Full key exchange flow: invite -> join -> complete -> poll
- Device listing and revocation endpoints
- Rate limiting: max 3 active invites per household
- Invite codes consumed after successful key exchange
New relay endpoints:
- POST /households/{id}/invite
- POST /invite/{code}/join
- GET /households/{id}/pending-exchanges
- POST /key-exchange/{id}/complete
- GET /key-exchange/{id}
- GET /households/{id}/devices
- DELETE /households/{id}/devices/{device_id}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The invite code is now sent in the request body (invite_code field)
instead of the URL path. This matches the spec's endpoint layout where
all household operations are under /households/{id}/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gate push/pull behind active Stripe subscription status, returning 402 when subscription is inactive. Add POST /webhooks/stripe endpoint with manual HMAC-SHA256 signature verification (no stripe-go dependency). Add GET /status endpoint for household sync status. Include MICASA_PRO_SPEC.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove sync_id_aliases (determined unnecessary in prior session) - Remove device.token file storage (token stored in sync_device table) - Remove phased timeline with week numbers and customer goals - Replace build plan with done/remaining/post-v1 breakdown - Mark key rotation as post-v1 (manual process documented instead) - Keep conflicts, storage, and blob sync in v1 scope - Remove unique constraint resolution's dependency on sync_id_aliases - Delete MICASA_PRO_SPEC_POSSIBLY_OLD.md (superseded by current spec: used integer PKs, Postgres/S3/Fly.io stack, weekly timelines) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Key rotation is table stakes — without it a compromised device retains cryptographic access to all future ops even after revocation. Restore the full key rotation design (`micasa pro keys rotate`) to v1 scope. Move device.token back to $XDG_DATA_HOME/micasa/keys/device.token (0600 permissions) alongside other key material. Storing the bearer token in the SQLite DB would mean `micasa backup backup.db` exports credentials, contradicting the spec's own security model that backups are data-only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GET /key-exchange/{id} endpoint is unauthenticated (the joiner has
no credentials yet). Previously it returned the device token and
encrypted household key on every call, meaning a leaked exchange ID
could be exploited indefinitely.
Now credentials are cleared from the store after the first successful
retrieval. A second GET returns ready=true but with empty token and
encrypted key fields. The exchange ID is effectively a one-time
capability token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename the credential storage directory from $XDG_DATA_HOME/micasa/keys/ to $XDG_DATA_HOME/micasa/secrets/ to make the sensitivity of its contents unmistakable. The directory holds household.key, device.key, device.pub, and device.token -- all material that must never be committed, cloud-backed-up, or included in SQLite backups. KeysDir() -> SecretsDir() with updated doc comment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix race condition on cached device ID with sync.Mutex - Set syncApplying context in ApplyOps to prevent infinite sync loops - Add table name allowlist to block remote ops targeting internal tables - Use errors.Is for errConflictLoss to handle wrapped errors - Zero key material after crypto operations with constant-time wipe - Tighten ciphertext minimum size checks (nonce + auth tag overhead) - Add Stringer redaction on HouseholdKey and DeviceKeyPair types - Validate loaded device public key against private key on load - Atomic file writes for device keypair persistence - Add request body size limits (1 MB) on all relay JSON endpoints - Validate public_key on household creation - Stop leaking internal error messages to HTTP clients - Reject Stripe webhook signatures with future timestamps - Return 200 (not 404) for unknown subscriptions in webhook handler - Increase invite code entropy from 40 to 64 bits - Propagate rand.Read errors in invite code generation - Use safeconv.Int for int64-to-int narrowing in RowCounts - Add ORDER BY tiebreakers to all oplog queries - Log JSON encoding errors instead of silently discarding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ages - Make conflict resolution atomic by moving applied_at clearing into the same transaction as remote op application - Fix off-by-one in invite attempt counting (> to >=) - Add household ID validation in join handler - Use errors.Is for http.ErrServerClosed check - Distinguish ErrRecordNotFound from other DB errors in cachedDeviceID - Fix DeleteIncident PreviousStatus assignment order - Propagate re-read error in UpdateDocumentExtraction - Reject fractional values in ParseStringID float64 case - Make SaveHouseholdKey use atomicWriteFile - Add HouseholdID to JoinResponse type - Return 200 for unparseable webhook bodies per Stripe best practice - Document intentional uint PK on ChatInput model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use os.CreateTemp with random suffix in atomicWriteFile to avoid predictable temp paths and concurrent write races - Add deferred os.Remove cleanup for temp file on failure paths - Return zero DeviceKeyPair on validation errors to prevent leaking loaded key material through error returns - Simplify zeroize to use clear() instead of allocating a zero slice - Convert allowedSyncTables mutable map to allowedSyncTable function - Add /relay binary to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GenerateDeviceKeyPair and LoadDeviceKeyPair were returning the partially-populated kp on errors after the private key had been written into it. A caller inspecting the return value despite the error could inadvertently use or log the private key material. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add householdID parameter to StartJoin interface and check it before incrementing usedAttempts, preventing attempt-burning attacks via mismatched household IDs in the URL - Log DB errors in cachedDeviceID instead of silently returning empty string, making failures observable - Reject Inf values in ParseStringID float64 case Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd sync device management Add the non-CLI foundation for micasa pro commands: - crypto: SaveDeviceToken/LoadDeviceToken with atomic writes and 0600 perms - sync: NewManagementClient constructor for management-only calls - sync: Client methods for all relay endpoints (CreateHousehold, Invite, Join, Status, ListDevices, RevokeDevice, key exchange) - data: RelayURL field on SyncDevice, GetSyncDevice/UpdateSyncDevice/ UpdateOplogDeviceIDs store methods, GormDB accessor for sync layer CLI wiring deferred until #785 (Cobra migration) merges. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire all pro subcommands (init, status, sync, invite, join, devices, devices revoke) into the Cobra command tree. Add workflow-oriented help text with examples to the pro command group. Extend styledHelp to render Cobra Example fields. Address roborev findings: add household-mismatch test proving invite attempts are not consumed, guard UpdateOplogDeviceIDs against empty IDs, document singleton invariant on UpdateSyncDevice, clarify GormDB bridge purpose, and document GetKeyExchangeResult unauthenticated-by-design security model. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Close store on all error paths in resolveProDeps and openAndMigrate - Add defer store.Close() in all runPro* functions - Replace busy-wait polling with signal-aware select loops (Ctrl+C safe) - Extract resolveRelayURL helper (flag > env > default precedence) - Add --relay-url flag to join command (was only on init) - Fix TestRunProJoin calls for 3-arg signature, add TestResolveRelayURL - Guard acceptExistingExtraction against empty DocID to fix TestExploreMode_AcceptWorksInExploreMode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e leaks Replace six repeated store.Close() calls in resolveProDeps with a single deferred closeOnErr guard pattern. Close setup stores in TestRunProInitAlreadyInitialized and TestRunProJoinAlreadyInHousehold before the SUT opens its own connection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement encrypted blob upload/download through the relay so documents
sync their file data across devices. BLOBs are encrypted with the
household key (NaCl secretbox) and content-addressed by SHA-256 hash.
Relay endpoints (PUT/GET/HEAD /blobs/{household_id}/{hash}) enforce auth,
subscription gating, hash validation, and a 1 GB per-household quota.
Client treats HTTP 409 (dedup) as success. Push flow uploads blobs for
newly-pushed document ops; pull flow fetches blobs for documents that
arrived without data (ChecksumSHA256 set but Data nil).
Also fixes a bug where blob_ref in oplog payloads would cause GORM
errors since it has no corresponding DB column -- now stripped before
insert/update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add client-side SHA-256 integrity verification in DownloadBlob after decryption to catch tampered or misaddressed blobs. Replace io.LimitReader with http.MaxBytesReader in handlePutBlob for proper 413 handling. Return error counts from uploadPendingBlobs/fetchPendingBlobs so runProSync can report partial blob failures. Change BlobStorage from pointer to value type since the relay always populates it. Document GORM soft-delete scoping on PendingBlobDocuments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hes in integrity errors Differentiate http.MaxBytesError (413) from other io.ReadAll failures (400) in handlePutBlob so network errors aren't misreported as size violations. Include expected and actual SHA-256 hashes in the blob integrity check error message for easier debugging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hal errors Reject applyInsert ops where the payload id does not match op.RowID to prevent ID spoofing from malicious remote ops. Add runtime.KeepAlive to crypto.zeroize so the compiler cannot elide clear(). Log unmarshal errors in uploadPendingBlobs instead of silently continuing. Document zero-knowledge hash design trade-off on handlePutBlob. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…essages Extract ID validation from applyInsert into a standalone helper that distinguishes missing/non-string ID from mismatched ID, improving debuggability. Replace the nil-panic test with table-driven tests on the extracted function covering match, mismatch, missing, and non-string cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exchange IDs are ephemeral coordination tokens, not database entities, so they don't need ULID's time-sortable property. Replace uid.New() with a 32-byte crypto/rand hex string (256-bit = 128-bit quantum security via Grover's), eliminating the predictable ULID timestamp prefix that narrows the search space. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AuthenticateDevice previously iterated all stored bcrypt hashes per request (~100ms each), creating a DoS vector that scales linearly with device count. Replace with a SHA-256 keyed index for O(1) lookup. Tokens are 256-bit crypto-random so a fast hash is safe -- bcrypt's slow hashing only protects low-entropy passwords, not high-entropy tokens. Relay test suite drops from ~0.6s to ~0.02s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ApplyOps now sorts DecryptedOps by Envelope.Seq before processing, ensuring causal ordering regardless of relay return order. The current MemStore returns ops in insertion order (which happens to match seq), but a future Firestore-backed store could return ops unordered without an explicit ORDER BY. Sorting client-side is cheap insurance against that class of bugs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies subscription bypass, blob upload with unlimited quota, blob download, and status reporting quota_bytes=0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-stage Dockerfile builds a static relay binary on alpine. Base compose runs postgres + relay with health checks. Caddy override adds automatic TLS via DOMAIN env var. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The relay binary always calls WithBlobQuota via parseBlobQuota, so the NewHandler default branch primarily serves tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ages - Dockerfile: run as non-root user (uid 10001), add .dockerignore - SELF_HOSTED: use strconv.ParseBool instead of exact "true" match - WithBlobQuota: panic on negative values instead of silent unlimited - runProStatus: deduplicate storage display via formatStorageUsage() - Sync race: set syncSyncing at call site before dispatching doSync - Surface blob errors and sync failures via setStatusError - Engine: return ctx.Err() after blob phases; fix concurrency doc - assert → require for precondition checks in integration test - pg.Close() before os.Exit on migration failure - Tests: truthy SELF_HOSTED variants, negative quota, custom quota, exitForm via sendKey, WithBlobQuota panic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove syncStartedMsg type and handler — status is now set at call sites before dispatching doSync, so the message is never produced - Fix .dockerignore: *_test.go → **/*_test.go for recursive match Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ServiceLogEntry has OnDelete:CASCADE on MaintenanceItem, which means a hard-delete of a maintenance item would silently remove child service logs at the DB level with no oplog entries — breaking sync. HardDeleteMaintenance follows the HardDeleteIncident pattern: - Enumerates all child ServiceLogEntries (including soft-deleted) - Detaches their documents (oplog update + DB persist) - Writes oplog delete entries for each child - Cleans up DeletionRecords for children and parent - Writes oplog delete entry for the parent - Hard-deletes the parent (CASCADE handles DB cleanup) TUI: extend D (hard-delete) keybinding to work on maintenance tab alongside incidents. Status bar prompt is entity-aware. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents linked directly to the maintenance item (EntityKind= "maintenance") were not being detached before hard-delete, leaving them orphaned with a stale entity reference. Now detaches both maintenance-item documents and service-log child documents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace silently discarded errors (items, _ :=) with require.NoError so failures produce clear assertion messages instead of index panics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- wrapcheck: wrap all external/interface errors with context - noctx: use NewRequestWithContext for production HTTP clients; file-level nolint for httptest in test files - errcheck/errchkjson: check json.Marshal and resp.Body.Close errors - revive: rename unused BeforeCreate tx params to _ - staticcheck: replace nil contexts with context.Background() - testifylint: assert.NotEmpty for IDs, require.Error for preconditions, assert.InDelta for floats, assert.Positive for > 0 - gosec: nolint for test credentials and weak random in test helpers - intrange: use range-over-int loops - ineffassign: remove dead rec assignment - goconst: extract repeated test string to constant - Bump google.golang.org/grpc 1.79.2 → 1.79.3 (GHSA-p77j-4mvh-x3m3) - Fix flaky TestNewIsTimeSorted with 1ms sleep between ULID generations - Update vendorHash in flake.nix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ac37fb4 to
8a190bf
Compare
- Transaction() propagates deviceCell to prevent nil-pointer on txStore.DeviceID() calls - UpdateDocument re-reads full row before writing oplog entry to avoid partial/corrupt payloads syncing to other devices - findOrCreateVendor writes oplog update for contact-field changes on existing vendors so edits propagate via sync - Validate public key length before copy in pro invite/join flows to prevent silent crypto failure from wrong-size keys - pushAll intersects relay-confirmed IDs with actually-pushed IDs before marking ops as synced Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move nolint:gosec directives to function call line (token.go, handler.go) so golangci-lint recognizes them - Move nolint:testifylint to assert.Equal line (oplog_test.go) - Skip file permission assertions on Windows (NTFS doesn't support Unix permissions) - Bump buger/jsonparser 1.1.1 → 1.1.2 (GHSA-6g7g-w4f8-9c9x) - Update vendorHash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pushAll returns len(ids) (actually synced) not len(pushResp.Confirmed) - Log warning when relay confirms unknown op IDs - wasExisting excludes soft-deleted vendors to avoid double oplog (restore + update) on vendor restore path - Use t.Skip instead of silent if-guard for Windows permission tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relay handler (56.5% → 63.6%): - 28 new tests covering error paths for handleStatus, handlePull, handlePutBlob, handleGetBlob, handleHeadBlob, handleCreateHousehold, handleCompleteKeyExchange, handleRevokeDevice, handleListDevices, handleGetPendingExchanges - failingStore wrapper for injecting store errors Sync engine (69.6% → 74.0%): - 10 new tests for uploadPendingBlobs and fetchPendingBlobs - Happy path, dedup skip, missing data, non-document ops, error counting, and combined upload+download in single cycle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused h *Handler param from createTestHouseholdDirect and all 12 call sites - Fix misleading doc comment on createTestHouseholdDirect - Unify hex encoding in engine_test.go to fmt.Sprintf, drop encoding/hex import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
relay (56.5% → 66.1%): - Join validation (name too long, missing invite, bad pubkey size) - Stripe webhook (oversized body, bad JSON, unknown sub, store error) - PutBlob oversized, requireSubscription error paths - failingStore: UpdateSubscription/HouseholdBySubscription overrides sync (69.6% → 76.2%): - 16 apply.go tests: allowedSyncTable, ApplyOps edge cases, applyOne error paths, duplicate insert, restore non-existent, bad JSON - uploadPendingBlobs/fetchPendingBlobs coverage already added crypto (83.5% → 87.2%): - atomicWriteFile error paths, Save*KeyPair bad directory - readBoundedFile: oversized, empty, not found, exact max - LoadDeviceKeyPair: wrong-size public/private keys data (80.5% → 80.6%): - findOrCreate empty/whitespace name - softDeleteWith non-existent ID - Restore flows for appliance/incident - UpdateDocument oplog with partial update - HardDeleteIncident with soft-deleted documents Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a pgtest job with a postgres:17 service container that runs PgStore integration tests (previously skipped due to missing DSN). Coverage uploaded to Codecov with a "postgres" flag. The pgstore_test.go file already exists with comprehensive tests for all Store interface methods — they just needed a real Postgres instance to run against. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- assert.Error → require.Error for error preconditions (testifylint) - nolint:gosec on intentional chmod in permission test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Postgres disallows FOR UPDATE with aggregate functions (COUNT). Lock the household row as the serialization point, then count invites without FOR UPDATE. Also fixes remaining lint: assert.Error → require.Error, gosec nolint on intentional chmod in tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -run TestPgStore filter meant only pgstore tests contributed to the coverage file. Run all relay tests so both MemStore and PgStore paths are in coverage-pg.txt, giving codecov a complete picture. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cpcloud
added a commit
that referenced
this pull request
Mar 19, 2026
## Summary - **ULID primary keys**: migrate all entities from auto-increment uint to ULID strings for sync compatibility - **Oplog**: change tracking via `sync_oplog_entries` table with GORM hooks for create/update/delete/restore - **Encryption**: NaCl secretbox (XSalsa20-Poly1305) envelope encryption with household key generation and NaCl box key exchange - **Relay server** (`cmd/relay`): full HTTP relay with PgStore, SHA-256 token auth, invite flow with FOR UPDATE locking, blob storage (upload/download/dedup/quota), Stripe webhook verification, graceful shutdown - **Sync engine** (`internal/sync`): pull/push with LWW conflict resolution, blob upload/download with encryption, `blob_ref` stripping in apply - **CLI**: `pro init`, `pro join`, `pro status`, `pro storage`, `pro sync`, `pro invite`, `pro devices`, `pro conflicts` commands - **TUI background sync**: indicator (synced/syncing/offline/conflict glyphs), debounce on mutations, deferred reload during form editing - **Self-hosted relay**: `SELF_HOSTED=true` mode with subscription bypass, configurable blob quota, Docker Compose stack (postgres + relay + optional Caddy TLS) - **CASCADE oplog safety**: `HardDeleteMaintenance` with proper oplog entries for child ServiceLogEntries and document detachment, TUI hard-delete extended to maintenance tab - **~18.7k lines** across 118 files, ~1:1 production-to-test ratio 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
cpcloud
added a commit
that referenced
this pull request
Mar 19, 2026
## Summary - **ULID primary keys**: migrate all entities from auto-increment uint to ULID strings for sync compatibility - **Oplog**: change tracking via `sync_oplog_entries` table with GORM hooks for create/update/delete/restore - **Encryption**: NaCl secretbox (XSalsa20-Poly1305) envelope encryption with household key generation and NaCl box key exchange - **Relay server** (`cmd/relay`): full HTTP relay with PgStore, SHA-256 token auth, invite flow with FOR UPDATE locking, blob storage (upload/download/dedup/quota), Stripe webhook verification, graceful shutdown - **Sync engine** (`internal/sync`): pull/push with LWW conflict resolution, blob upload/download with encryption, `blob_ref` stripping in apply - **CLI**: `pro init`, `pro join`, `pro status`, `pro storage`, `pro sync`, `pro invite`, `pro devices`, `pro conflicts` commands - **TUI background sync**: indicator (synced/syncing/offline/conflict glyphs), debounce on mutations, deferred reload during form editing - **Self-hosted relay**: `SELF_HOSTED=true` mode with subscription bypass, configurable blob quota, Docker Compose stack (postgres + relay + optional Caddy TLS) - **CASCADE oplog safety**: `HardDeleteMaintenance` with proper oplog entries for child ServiceLogEntries and document detachment, TUI hard-delete extended to maintenance tab - **~18.7k lines** across 118 files, ~1:1 production-to-test ratio 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
cpcloud
added a commit
to cpcloud/micasa
that referenced
this pull request
Mar 24, 2026
…v#791) Thread context.Context through all sync Client methods (Push, Pull, blob ops, household management) so callers can cancel in-flight HTTP requests on shutdown. Fix PutBlob quota TOCTOU race with FOR UPDATE lock on household row. Return explicit error on second key-exchange credential retrieval instead of ambiguous empty response. Move invite attempt counter increment after key-exchange creation so valid joins don't consume brute-force slots. Make FTS update trigger soft-delete aware (skip re-indexing deleted docs, handle restore correctly). Log non-subscription webhook event types at info level. Guard BlobRef oplog field with non-empty checksum check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
sync_oplog_entriestable with GORM hooks for create/update/delete/restorecmd/relay): full HTTP relay with PgStore, SHA-256 token auth, invite flow with FOR UPDATE locking, blob storage (upload/download/dedup/quota), Stripe webhook verification, graceful shutdowninternal/sync): pull/push with LWW conflict resolution, blob upload/download with encryption,blob_refstripping in applypro init,pro join,pro status,pro storage,pro sync,pro invite,pro devices,pro conflictscommandsSELF_HOSTED=truemode with subscription bypass, configurable blob quota, Docker Compose stack (postgres + relay + optional Caddy TLS)HardDeleteMaintenancewith proper oplog entries for child ServiceLogEntries and document detachment, TUI hard-delete extended to maintenance tab🤖 Generated with Claude Code