Merge branch 'main' of ssh://gitea.jeffemmett.com:223/jeffemmett/rspace-online

This commit is contained in:
Jeff Emmett 2026-03-12 14:00:23 -07:00
commit 00e5229ef7
3 changed files with 163 additions and 8 deletions

View File

@ -78,6 +78,15 @@ export {
type AccountVault, type AccountVault,
} from './vault'; } from './vault';
// ============================================================================
// WALLET SYNC
// ============================================================================
export {
syncWalletsOnLogin,
syncWalletsToVault,
} from './wallet-sync';
// ============================================================================ // ============================================================================
// UI COMPONENTS // UI COMPONENTS
// ============================================================================ // ============================================================================

View File

@ -16,6 +16,7 @@ import { getKeyManager } from '../key-derivation';
import { getSessionManager, AuthLevel } from '../session'; import { getSessionManager, AuthLevel } from '../session';
import { getDocBridge, resetDocBridge } from '../../../shared/local-first/encryptid-bridge'; import { getDocBridge, resetDocBridge } from '../../../shared/local-first/encryptid-bridge';
import { getVaultManager, resetVaultManager } from '../vault'; import { getVaultManager, resetVaultManager } from '../vault';
import { syncWalletsOnLogin } from '../wallet-sync';
// ============================================================================ // ============================================================================
// STYLES // STYLES
@ -415,9 +416,9 @@ export class EncryptIDLoginButton extends HTMLElement {
// Load encrypted account vault in background // Load encrypted account vault in background
const docCrypto = getDocBridge().getDocCrypto(); const docCrypto = getDocBridge().getDocCrypto();
if (docCrypto) { if (docCrypto) {
getVaultManager(docCrypto).load().catch(err => getVaultManager(docCrypto).load()
console.warn('Vault load failed:', err) .then(() => syncWalletsOnLogin(docCrypto))
); .catch(err => console.warn('Vault load failed:', err));
} }
} }
@ -429,7 +430,7 @@ export class EncryptIDLoginButton extends HTMLElement {
await sessionManager.createSession(result, keys.did, { await sessionManager.createSession(result, keys.did, {
encrypt: true, encrypt: true,
sign: true, sign: true,
wallet: false, // Will be true after wallet setup wallet: !!keys.eoaAddress,
}); });
// Dispatch success event // Dispatch success event
@ -534,9 +535,9 @@ export class EncryptIDLoginButton extends HTMLElement {
// Load encrypted account vault in background // Load encrypted account vault in background
const docCrypto = getDocBridge().getDocCrypto(); const docCrypto = getDocBridge().getDocCrypto();
if (docCrypto) { if (docCrypto) {
getVaultManager(docCrypto).load().catch(err => getVaultManager(docCrypto).load()
console.warn('Vault load failed:', err) .then(() => syncWalletsOnLogin(docCrypto))
); .catch(err => console.warn('Vault load failed:', err));
} }
} }
@ -546,7 +547,7 @@ export class EncryptIDLoginButton extends HTMLElement {
await sessionManager.createSession(result, keys.did, { await sessionManager.createSession(result, keys.did, {
encrypt: true, encrypt: true,
sign: true, sign: true,
wallet: false, wallet: !!keys.eoaAddress,
}); });
this.dispatchEvent(new CustomEvent('login-success', { this.dispatchEvent(new CustomEvent('login-success', {

View File

@ -0,0 +1,145 @@
/**
* EncryptID Wallet Sync
*
* Bridges WalletStore (encrypted localStorage) VaultManager (encrypted server blob)
* so wallet associations survive across devices.
*
* On login:
* 1. Load vault from server
* 2. Merge vault wallets into local WalletStore (new entries only)
* 3. Merge local wallets into vault (in case this device has wallets the vault doesn't)
* 4. Save vault if anything changed
*
* On wallet add/remove:
* syncWalletsToVault() pushes current WalletStore state into the vault.
*/
import { getWalletStore, type SafeWalletEntry } from './wallet-store';
import { getVaultManager, type AccountVault } from './vault';
import { getKeyManager } from './key-derivation';
import type { DocCrypto } from '../../shared/local-first/crypto';
/**
* After login + vault load, merge wallets bidirectionally between
* the local WalletStore and the remote AccountVault.
*
* Requires PRF-derived encryption key (for WalletStore) and DocCrypto (for VaultManager).
*/
export async function syncWalletsOnLogin(docCrypto: DocCrypto): Promise<void> {
try {
const km = getKeyManager();
if (!km.isInitialized()) return;
const keys = await km.getKeys();
if (!keys.encryptionKey) return;
const walletStore = getWalletStore(keys.encryptionKey);
const vaultManager = getVaultManager(docCrypto);
const vault = await vaultManager.get();
const localWallets = await walletStore.list();
const vaultWallets = vault.wallets || [];
let changed = false;
// 1. Restore vault wallets → local WalletStore (if not already present)
for (const vw of vaultWallets) {
const exists = localWallets.find(
lw => lw.safeAddress.toLowerCase() === vw.safeAddress.toLowerCase()
&& lw.chainId === vw.chainId
);
if (!exists) {
await walletStore.add({
safeAddress: vw.safeAddress,
chainId: vw.chainId,
eoaAddress: vw.eoaAddress,
label: vw.label,
});
}
}
// 2. Push local wallets → vault (if not already present)
const updatedLocal = await walletStore.list();
const updatedVaultWallets = [...vaultWallets];
for (const lw of updatedLocal) {
const exists = updatedVaultWallets.find(
vw => vw.safeAddress.toLowerCase() === lw.safeAddress.toLowerCase()
&& vw.chainId === lw.chainId
);
if (!exists) {
updatedVaultWallets.push({
id: lw.id,
safeAddress: lw.safeAddress,
chainId: lw.chainId,
eoaAddress: lw.eoaAddress,
label: lw.label,
addedAt: lw.addedAt,
});
changed = true;
}
}
// 3. Also ensure the server-side wallet_address profile field is set
// This is the fallback for mobile (no PRF) — the JWT carries it.
if (keys.eoaAddress) {
await setServerWalletAddress(keys.eoaAddress).catch(() => {});
}
// 4. Save vault if anything changed
if (changed) {
await vaultManager.update({ wallets: updatedVaultWallets });
}
} catch (err) {
console.warn('Wallet sync failed:', err);
}
}
/**
* Push current WalletStore state into the vault.
* Call this after any wallet add/update/remove operation.
*/
export async function syncWalletsToVault(docCrypto: DocCrypto): Promise<void> {
try {
const km = getKeyManager();
if (!km.isInitialized()) return;
const keys = await km.getKeys();
if (!keys.encryptionKey) return;
const walletStore = getWalletStore(keys.encryptionKey);
const vaultManager = getVaultManager(docCrypto);
const localWallets = await walletStore.list();
const vaultWallets = localWallets.map(lw => ({
id: lw.id,
safeAddress: lw.safeAddress,
chainId: lw.chainId,
eoaAddress: lw.eoaAddress,
label: lw.label,
addedAt: lw.addedAt,
}));
await vaultManager.update({ wallets: vaultWallets });
} catch (err) {
console.warn('Wallet vault sync failed:', err);
}
}
/**
* Set the wallet address on the server user profile.
* This ensures the JWT carries the wallet address for non-PRF devices (mobile).
*/
async function setServerWalletAddress(walletAddress: string): Promise<void> {
const token = localStorage.getItem('encryptid_token');
if (!token) return;
await fetch('/encryptid/api/wallet-capability', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ walletAddress }),
});
}