Files
bitvid/js/services/storageSyncService.js
T
thePR0M3TH3ANandClaude Opus 4.8 d552e9017c feat(sync): NWC wallet sync (opt-in, spend-gated) + shared opt-in flags
Adds encrypted cross-login sync for the NWC wallet connection (todo #15),
reusing the encrypted-sync core under d-tag "bitvid:nwc".

- js/services/settingsSyncFlags.js: shared per-pubkey opt-in flag store
  (storage/wallet); storageSyncService refactored onto it (identical behavior,
  tests still green).
- js/services/walletSyncService.js: factory (NwcSettingsService is per-app, not a
  singleton). push() syncs the current nwcUri (+defaultZap); pull() applies it via
  updateActiveNwcSettings; disable() wipes the note.
- Wallet pane (components/profile-modal.html + ProfileWalletController): opt-in
  toggle + Restore button + a strong SPENDING warning, shown only when a signer
  can encrypt. Enabling requires an explicit window.confirm (the URI can spend).
  Re-pushes on save when enabled; and on wallet DISCONNECT while synced it clears
  the note so a removed spending URI doesn't linger (encrypted) on relays.

tests/wallet-sync-service.test.mjs: enable-pushes-uri, pull-restores-via-
updateActiveNwcSettings, disable-clears, no-wallet-doesn't-publish. Mutation-
verified (skip-apply + skip-clear both fail).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 17:57:39 -04:00

103 lines
3.2 KiB
JavaScript

// Opt-in encrypted cross-login sync for STORAGE credentials (todo #15, step 2).
// Wraps the encrypted-sync core: the on-disk storage account record (already
// self-protected, but with plaintext meta) is re-encrypted to self and published
// as a NIP-78 replaceable note under a stable d-tag, then pulled + imported on
// another device.
//
// Opt-in only, off by default. A per-pubkey local flag remembers the choice so
// the UI toggle persists and a future "re-push on change" knows whether to sync.
import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
import defaultStorageService from "./storageService.js";
import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
import { userLogger } from "../utils/logger.js";
export const STORAGE_SYNC_DTAG = "bitvid:storage-connections";
const SYNC_KIND = "storage";
export function createStorageSyncService({
encryptedSync = defaultEncryptedSync,
storage = defaultStorageService,
} = {}) {
function normalizePubkey(pubkey) {
return typeof pubkey === "string" ? pubkey.trim() : "";
}
function isAvailable() {
return Boolean(encryptedSync?.isAvailable?.());
}
function isEnabled(pubkey) {
return isSyncEnabled(normalizePubkey(pubkey), SYNC_KIND);
}
function setEnabledFlag(pubkey, enabled) {
setSyncEnabled(normalizePubkey(pubkey), SYNC_KIND, enabled);
}
// Push the current local storage account record to the user's encrypted note.
async function push(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return { ok: false, error: "missing-pubkey" };
}
let record;
try {
record = await storage.exportAccountRecord(key);
} catch (error) {
userLogger.warn("[storageSync] Failed to export storage record:", error);
return { ok: false, error: "export-failed", cause: error };
}
if (!record) {
return { ok: false, error: "nothing-to-sync" };
}
return encryptedSync.push(STORAGE_SYNC_DTAG, record);
}
// Pull the encrypted note and import it into local storage. Returns the
// encrypted-sync pull result with `imported: true` when a record was applied.
async function pull(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return { found: false, error: "missing-pubkey" };
}
const result = await encryptedSync.pull(STORAGE_SYNC_DTAG);
if (!result?.found) {
return result;
}
try {
await storage.importAccountRecord(key, result.payload);
} catch (error) {
userLogger.warn("[storageSync] Failed to import storage record:", error);
return { ...result, imported: false, error: "import-failed", cause: error };
}
return { ...result, imported: true };
}
// Turn sync on: remember the choice and push immediately.
async function enable(pubkey) {
setEnabledFlag(pubkey, true);
return push(pubkey);
}
// Turn sync off: remember the choice and wipe the published note.
async function disable(pubkey) {
setEnabledFlag(pubkey, false);
return encryptedSync.clear(STORAGE_SYNC_DTAG);
}
return {
STORAGE_SYNC_DTAG,
isAvailable,
isEnabled,
push,
pull,
enable,
disable,
};
}
export const storageSyncService = createStorageSyncService();
export default storageSyncService;