feat(sync): storage-credential sync data layer (push/pull/clear)

Wires the encrypted-sync core (0abb82b9) to the live client + active signer and
adds the storage-credential sync service. No UI yet — the Storage pane toggle is
the next increment.

- js/nostr/encryptedSyncFacade.js: default encryptedSync instance bound to
  nostrClient (write set via getDeletePublishRelays, read relays, pool) and the
  active signer's signEvent. Keeps the core pure/testable.
- js/services/storageService.js: exportAccountRecord / importAccountRecord — get
  the on-disk account record and restore it on another device. Import REPLACES
  the local record (same-pubkey envelope unlocks with the same signer); cross-
  device merge is a follow-up.
- js/services/storageSyncService.js: enable/disable/push/pull under d-tag
  "bitvid:storage-connections", with a per-pubkey opt-in flag (off by default).
  enable() pushes immediately; disable() wipes the published note.

tests/storage-sync-service.test.mjs: enable-pushes-exported-record, pull-restores
on a fresh device, disable-clears, empty-record-doesn't-publish. Mutation-verified
(skip-import and skip-clear both fail).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-21 17:01:54 -04:00
1 parent 0abb82b933
commit 6f7f419636
4 files changed
+366

No files matched your search

+36
View File
@@ -611,6 +611,42 @@ export class StorageService {
}
}
/**
* Exports the raw on-disk account record for a pubkey, for opt-in encrypted
* cross-login sync (todo #15). The record is already self-protected
* (encryptedMasterKey envelope + per-connection AES-GCM payloads) but its
* `meta` is plaintext (bucket/endpoint names), so callers MUST re-encrypt the
* whole record to self before publishing. Returns null when nothing is stored.
*/
async exportAccountRecord(pubkey) {
if (!pubkey) {
return null;
}
const account = await this._getAccount(pubkey);
return account || null;
}
/**
* Imports an account record previously exported via {@link exportAccountRecord}
* (e.g. pulled from the user's encrypted Nostr note on a second device). The
* record is encrypted-to-self by the SAME pubkey, so its envelope unlocks with
* the same signer. This REPLACES the local record for the pubkey; the next
* unlock decrypts the imported master key. (Merge across devices is a follow-up.)
*/
async importAccountRecord(pubkey, record) {
if (!pubkey) {
throw new Error("Pubkey required to import storage account record.");
}
if (!record || typeof record !== "object" || !record.encryptedMasterKey) {
throw new Error("Invalid storage account record.");
}
const toSave = { ...record, pubkey };
await this._saveAccount(toSave);
// Drop any in-memory master key so the next unlock uses the imported envelope.
this.lock(pubkey);
return toSave;
}
/**
* Sets a connection as default for uploads (updates metadata).
*/