Merge branch 'dev'
This commit is contained in:
commit
469d7e2247
|
|
@ -67,6 +67,17 @@ export {
|
|||
type RecoveryRequest,
|
||||
} from './recovery';
|
||||
|
||||
// ============================================================================
|
||||
// ENCRYPTED VAULT
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
VaultManager,
|
||||
getVaultManager,
|
||||
resetVaultManager,
|
||||
type AccountVault,
|
||||
} from './vault';
|
||||
|
||||
// ============================================================================
|
||||
// UI COMPONENTS
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -2759,8 +2759,8 @@ app.get('/', (c) => {
|
|||
.passkey-date { color: #64748b; }
|
||||
|
||||
/* Recovery email */
|
||||
.recovery-section, .guardians-section, .devices-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.08); }
|
||||
.recovery-section h4, .guardians-section h4, .devices-section h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||
.recovery-section, .guardians-section, .devices-section, .vault-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.08); }
|
||||
.recovery-section h4, .guardians-section h4, .devices-section h4, .vault-section h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||
.recovery-row { display: flex; gap: 0.5rem; }
|
||||
.recovery-row input { flex: 1; }
|
||||
.recovery-row button { white-space: nowrap; }
|
||||
|
|
@ -2891,6 +2891,7 @@ app.get('/', (c) => {
|
|||
<div class="check-item"><div class="check-icon" id="check-email">○</div> <span id="check-email-text">Recovery email</span></div>
|
||||
<div class="check-item"><div class="check-icon" id="check-device">○</div> <span id="check-device-text">Second device</span></div>
|
||||
<div class="check-item"><div class="check-icon" id="check-guardians">○</div> <span id="check-guardians-text">Guardians (0/3)</span></div>
|
||||
<div class="check-item"><div class="check-icon" id="check-vault">○</div> <span id="check-vault-text">Encrypted vault backup</span></div>
|
||||
</div>
|
||||
|
||||
<div class="recovery-section">
|
||||
|
|
@ -2941,6 +2942,17 @@ app.get('/', (c) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vault-section">
|
||||
<h4>Encrypted Account Vault</h4>
|
||||
<p style="font-size:0.8rem;color:#64748b;margin-bottom:0.75rem">Your account data is encrypted with your passkey and stored securely. Only you can unlock it.</p>
|
||||
<div id="vault-status" style="font-size:0.8rem;padding:0.5rem 0.75rem;border-radius:0.5rem;background:rgba(255,255,255,0.04);color:#94a3b8">Status: Not synced</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:0.75rem">
|
||||
<button class="btn-secondary" onclick="saveVault()">Save to Encrypted Server</button>
|
||||
<button class="btn-secondary" onclick="restoreVault()">Restore from Server</button>
|
||||
</div>
|
||||
<div id="vault-msg" style="font-size:0.8rem;margin-top:0.5rem;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<a href="/demo.html" class="btn-secondary">SDK Demo</a>
|
||||
<button class="btn-secondary btn-danger" onclick="handleLogout()">Sign Out</button>
|
||||
|
|
@ -2995,6 +3007,7 @@ app.get('/', (c) => {
|
|||
getKeyManager,
|
||||
getSessionManager,
|
||||
detectCapabilities,
|
||||
authenticatePasskey,
|
||||
} from '/dist/index.js';
|
||||
|
||||
const TOKEN_KEY = 'encryptid_token';
|
||||
|
|
@ -3198,8 +3211,9 @@ app.get('/', (c) => {
|
|||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Load guardians
|
||||
// Load guardians and check vault status
|
||||
loadGuardians();
|
||||
checkVaultStatus();
|
||||
}
|
||||
|
||||
window.handleLogout = () => {
|
||||
|
|
@ -3389,6 +3403,155 @@ app.get('/', (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Vault helpers ──
|
||||
|
||||
const VAULT_SPACE = '__vault';
|
||||
const VAULT_DOC = 'account-vault';
|
||||
const enc = new TextEncoder();
|
||||
|
||||
async function deriveVaultKey(prfOutput) {
|
||||
// Import PRF output as HKDF material
|
||||
const master = await crypto.subtle.importKey('raw', prfOutput, { name: 'HKDF' }, false, ['deriveKey', 'deriveBits']);
|
||||
// Derive space key: info = "rspace:__vault"
|
||||
const spaceBits = await crypto.subtle.deriveBits(
|
||||
{ name: 'HKDF', hash: 'SHA-256', salt: enc.encode('rspace-space-key-v1'), info: enc.encode('rspace:' + VAULT_SPACE) },
|
||||
master, 256
|
||||
);
|
||||
const spaceKey = await crypto.subtle.importKey('raw', spaceBits, { name: 'HKDF' }, false, ['deriveKey', 'deriveBits']);
|
||||
// Derive doc key: info = "doc:account-vault"
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: 'HKDF', hash: 'SHA-256', salt: enc.encode('rspace-doc-key-v1'), info: enc.encode('doc:' + VAULT_DOC) },
|
||||
spaceKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
function vaultMsg(text, ok) {
|
||||
const el = document.getElementById('vault-msg');
|
||||
el.textContent = text;
|
||||
el.style.color = ok ? '#86efac' : '#fca5a5';
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function gatherVaultData() {
|
||||
const username = document.getElementById('profile-username')?.textContent || null;
|
||||
const email = document.getElementById('recovery-email')?.value?.trim() || null;
|
||||
return {
|
||||
version: 1,
|
||||
profile: { displayName: username, avatarUrl: null },
|
||||
emails: email ? [{ address: email, verified: false, addedAt: Date.now() }] : [],
|
||||
devices: [],
|
||||
addresses: [],
|
||||
wallets: [],
|
||||
preferences: {},
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
window.saveVault = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) { vaultMsg('Not authenticated', false); return; }
|
||||
try {
|
||||
vaultMsg('Authenticate with your passkey to encrypt...', true);
|
||||
const result = await authenticatePasskey();
|
||||
if (!result.prfOutput) { vaultMsg('PRF not available — vault requires a PRF-capable passkey', false); return; }
|
||||
|
||||
const key = await deriveVaultKey(result.prfOutput);
|
||||
const vault = gatherVaultData();
|
||||
const json = new TextEncoder().encode(JSON.stringify(vault));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, json));
|
||||
// Pack: [12-byte nonce][ciphertext]
|
||||
const packed = new Uint8Array(12 + ct.length);
|
||||
packed.set(nonce, 0);
|
||||
packed.set(ct, 12);
|
||||
|
||||
const res = await fetch('/api/backup/' + VAULT_SPACE + '/' + VAULT_DOC, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/octet-stream' },
|
||||
body: packed,
|
||||
});
|
||||
if (!res.ok) throw new Error('Server returned ' + res.status);
|
||||
vaultMsg('Vault saved and encrypted successfully.', true);
|
||||
updateVaultChecklist(true);
|
||||
document.getElementById('vault-status').textContent = 'Status: Synced — ' + new Date().toLocaleString();
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') { vaultMsg('Passkey authentication cancelled.', false); return; }
|
||||
vaultMsg('Save failed: ' + err.message, false);
|
||||
}
|
||||
};
|
||||
|
||||
window.restoreVault = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) { vaultMsg('Not authenticated', false); return; }
|
||||
try {
|
||||
vaultMsg('Authenticate with your passkey to decrypt...', true);
|
||||
const result = await authenticatePasskey();
|
||||
if (!result.prfOutput) { vaultMsg('PRF not available — vault requires a PRF-capable passkey', false); return; }
|
||||
|
||||
const blobRes = await fetch('/api/backup/' + VAULT_SPACE + '/' + VAULT_DOC, {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (blobRes.status === 404) { vaultMsg('No vault backup found on server.', false); return; }
|
||||
if (!blobRes.ok) throw new Error('Server returned ' + blobRes.status);
|
||||
|
||||
const packed = new Uint8Array(await blobRes.arrayBuffer());
|
||||
if (packed.length < 13) throw new Error('Invalid vault data');
|
||||
const nonce = packed.slice(0, 12);
|
||||
const ct = packed.slice(12);
|
||||
|
||||
const key = await deriveVaultKey(result.prfOutput);
|
||||
const plainBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, key, ct);
|
||||
const vault = JSON.parse(new TextDecoder().decode(plainBuf));
|
||||
|
||||
// Populate DOM
|
||||
if (vault.profile?.displayName) {
|
||||
const el = document.getElementById('profile-username');
|
||||
if (el) el.textContent = vault.profile.displayName;
|
||||
}
|
||||
if (vault.emails?.length > 0) {
|
||||
const el = document.getElementById('recovery-email');
|
||||
if (el) el.value = vault.emails[0].address;
|
||||
}
|
||||
vaultMsg('Vault restored successfully. ' + (vault.emails?.length || 0) + ' email(s), ' + (vault.wallets?.length || 0) + ' wallet(s).', true);
|
||||
updateVaultChecklist(true);
|
||||
document.getElementById('vault-status').textContent = 'Status: Restored — ' + new Date(vault.updatedAt).toLocaleString();
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') { vaultMsg('Passkey authentication cancelled.', false); return; }
|
||||
vaultMsg('Restore failed: ' + err.message, false);
|
||||
}
|
||||
};
|
||||
|
||||
async function checkVaultStatus() {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch('/api/backup/' + VAULT_SPACE + '/' + VAULT_DOC, {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) {
|
||||
updateVaultChecklist(true);
|
||||
document.getElementById('vault-status').textContent = 'Status: Backup exists on server';
|
||||
} else {
|
||||
updateVaultChecklist(false);
|
||||
}
|
||||
} catch { /* offline or error */ }
|
||||
}
|
||||
|
||||
function updateVaultChecklist(exists) {
|
||||
const icon = document.getElementById('check-vault');
|
||||
const text = document.getElementById('check-vault-text');
|
||||
if (exists) {
|
||||
icon.className = 'check-icon done';
|
||||
icon.innerHTML = '✓';
|
||||
text.textContent = 'Encrypted vault backup';
|
||||
} else {
|
||||
icon.className = 'check-icon todo';
|
||||
icon.innerHTML = '○';
|
||||
text.textContent = 'Encrypted vault backup';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Checklist update ──
|
||||
|
||||
function updateChecklist(guardians) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { getKeyManager } from '../key-derivation';
|
||||
import { getSessionManager, AuthLevel } from '../session';
|
||||
import { getDocBridge, resetDocBridge } from '../../../shared/local-first/encryptid-bridge';
|
||||
import { getVaultManager, resetVaultManager } from '../vault';
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
|
|
@ -411,6 +412,13 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
await keyManager.initFromPRF(result.prfOutput);
|
||||
// Initialize doc encryption bridge for local-first encrypted storage
|
||||
await getDocBridge().initFromAuth(result.prfOutput);
|
||||
// Load encrypted account vault in background
|
||||
const docCrypto = getDocBridge().getDocCrypto();
|
||||
if (docCrypto) {
|
||||
getVaultManager(docCrypto).load().catch(err =>
|
||||
console.warn('Vault load failed:', err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get derived keys
|
||||
|
|
@ -505,7 +513,8 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
const keyManager = getKeyManager();
|
||||
keyManager.clear();
|
||||
|
||||
// Clear doc encryption bridge key material
|
||||
// Clear vault and doc encryption bridge key material
|
||||
resetVaultManager();
|
||||
resetDocBridge();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('logout', { bubbles: true }));
|
||||
|
|
@ -522,6 +531,13 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
if (result.prfOutput) {
|
||||
await keyManager.initFromPRF(result.prfOutput);
|
||||
await getDocBridge().initFromAuth(result.prfOutput);
|
||||
// Load encrypted account vault in background
|
||||
const docCrypto = getDocBridge().getDocCrypto();
|
||||
if (docCrypto) {
|
||||
getVaultManager(docCrypto).load().catch(err =>
|
||||
console.warn('Vault load failed:', err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const keys = await keyManager.getKeys();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* EncryptID Encrypted Account Vault
|
||||
*
|
||||
* Stores all account data (profile, emails, devices, addresses, wallets,
|
||||
* preferences) as a single JSON document, encrypted client-side with
|
||||
* AES-256-GCM using the DocCrypto key hierarchy, and persisted as an
|
||||
* opaque blob via the backup API. The server never sees plaintext.
|
||||
*
|
||||
* Key derivation:
|
||||
* Master Key (from WebAuthn PRF)
|
||||
* → deriveSpaceKey("__vault")
|
||||
* → deriveDocKey(spaceKey, "account-vault")
|
||||
* → AES-256-GCM encrypt/decrypt
|
||||
*
|
||||
* Any device with the user's passkey derives the same vault key
|
||||
* (deterministic HKDF), so no key sync is needed.
|
||||
*/
|
||||
|
||||
import { DocCrypto } from '../../shared/local-first/crypto';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface AccountVault {
|
||||
version: 1;
|
||||
profile: { displayName: string | null; avatarUrl: string | null };
|
||||
emails: Array<{ address: string; verified: boolean; addedAt: number }>;
|
||||
devices: Array<{ credentialIdPrefix: string; name: string; platform: string; addedAt: number }>;
|
||||
addresses: Array<{
|
||||
id: string; label: string; street1: string; city: string;
|
||||
state: string; postalCode: string; country: string;
|
||||
}>;
|
||||
wallets: Array<{
|
||||
id: string; safeAddress: string; chainId: number;
|
||||
eoaAddress: string; label: string; addedAt: number;
|
||||
}>;
|
||||
preferences: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const VAULT_SPACE = '__vault';
|
||||
const VAULT_DOC = 'account-vault';
|
||||
const CACHE_KEY = 'encryptid_vault_cache';
|
||||
const TOKEN_KEY = 'encryptid_token';
|
||||
|
||||
// ============================================================================
|
||||
// VaultManager
|
||||
// ============================================================================
|
||||
|
||||
export class VaultManager {
|
||||
private crypto: DocCrypto;
|
||||
private vaultKey: CryptoKey | null = null;
|
||||
private vault: AccountVault | null = null;
|
||||
|
||||
constructor(docCrypto: DocCrypto) {
|
||||
this.crypto = docCrypto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the deterministic vault key.
|
||||
*/
|
||||
private async getVaultKey(): Promise<CryptoKey> {
|
||||
if (!this.vaultKey) {
|
||||
this.vaultKey = await this.crypto.deriveDocKeyDirect(VAULT_SPACE, VAULT_DOC);
|
||||
}
|
||||
return this.vaultKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auth token for backup API calls.
|
||||
*/
|
||||
private getToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load vault from server (or localStorage cache if offline).
|
||||
* On 404: returns empty vault. Caches encrypted blob locally for offline.
|
||||
*/
|
||||
async load(): Promise<AccountVault> {
|
||||
const key = await this.getVaultKey();
|
||||
const token = this.getToken();
|
||||
|
||||
let packed: Uint8Array | null = null;
|
||||
|
||||
// Try server first
|
||||
if (token) {
|
||||
try {
|
||||
const res = await fetch(`/api/backup/${VAULT_SPACE}/${VAULT_DOC}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (res.ok) {
|
||||
packed = new Uint8Array(await res.arrayBuffer());
|
||||
// Cache encrypted blob locally
|
||||
this.cacheBlob(packed);
|
||||
} else if (res.status !== 404) {
|
||||
console.warn('Vault: server returned', res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Vault: server fetch failed, trying cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage cache
|
||||
if (!packed) {
|
||||
packed = this.loadCachedBlob();
|
||||
}
|
||||
|
||||
if (!packed || packed.length < 13) {
|
||||
// No vault exists yet — return empty
|
||||
this.vault = VaultManager.empty();
|
||||
return this.vault;
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
const blob = DocCrypto.unpack(packed);
|
||||
const plainBytes = await this.crypto.decrypt(key, blob);
|
||||
const json = new TextDecoder().decode(plainBytes);
|
||||
this.vault = JSON.parse(json) as AccountVault;
|
||||
return this.vault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save vault to server (and localStorage cache).
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
if (!this.vault) return;
|
||||
|
||||
const key = await this.getVaultKey();
|
||||
const token = this.getToken();
|
||||
if (!token) throw new Error('Vault: not authenticated');
|
||||
|
||||
this.vault.updatedAt = Date.now();
|
||||
|
||||
const json = JSON.stringify(this.vault);
|
||||
const plainBytes = new TextEncoder().encode(json);
|
||||
const blob = await this.crypto.encrypt(key, plainBytes);
|
||||
const packed = DocCrypto.pack(blob);
|
||||
|
||||
// Upload to server
|
||||
const res = await fetch(`/api/backup/${VAULT_SPACE}/${VAULT_DOC}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: packed,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error((data as any).error || `Vault save failed (${res.status})`);
|
||||
}
|
||||
|
||||
// Update local cache
|
||||
this.cacheBlob(packed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the in-memory vault (loads from server/cache if needed).
|
||||
*/
|
||||
async get(): Promise<AccountVault> {
|
||||
if (!this.vault) {
|
||||
await this.load();
|
||||
}
|
||||
return this.vault!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge partial updates into the vault and auto-save.
|
||||
*/
|
||||
async update(partial: Partial<Omit<AccountVault, 'version' | 'createdAt' | 'updatedAt'>>): Promise<AccountVault> {
|
||||
const v = await this.get();
|
||||
Object.assign(v, partial);
|
||||
await this.save();
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a vault exists on the server (without downloading full blob).
|
||||
*/
|
||||
async exists(): Promise<boolean> {
|
||||
const token = this.getToken();
|
||||
if (!token) return false;
|
||||
try {
|
||||
const res = await fetch(`/api/backup/${VAULT_SPACE}/${VAULT_DOC}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear in-memory state (on logout). Does NOT delete server data.
|
||||
*/
|
||||
clear(): void {
|
||||
this.vault = null;
|
||||
this.vaultKey = null;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PRIVATE: localStorage cache
|
||||
// ==========================================================================
|
||||
|
||||
private cacheBlob(packed: Uint8Array): void {
|
||||
try {
|
||||
// Store as base64
|
||||
const binary = String.fromCharCode(...packed);
|
||||
localStorage.setItem(CACHE_KEY, btoa(binary));
|
||||
} catch {
|
||||
// localStorage full or unavailable
|
||||
}
|
||||
}
|
||||
|
||||
private loadCachedBlob(): Uint8Array | null {
|
||||
try {
|
||||
const b64 = localStorage.getItem(CACHE_KEY);
|
||||
if (!b64) return null;
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STATIC: empty vault
|
||||
// ==========================================================================
|
||||
|
||||
static empty(): AccountVault {
|
||||
const now = Date.now();
|
||||
return {
|
||||
version: 1,
|
||||
profile: { displayName: null, avatarUrl: null },
|
||||
emails: [],
|
||||
devices: [],
|
||||
addresses: [],
|
||||
wallets: [],
|
||||
preferences: {},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SINGLETON
|
||||
// ============================================================================
|
||||
|
||||
let vaultInstance: VaultManager | null = null;
|
||||
|
||||
/**
|
||||
* Get or create the vault manager singleton.
|
||||
* Requires a DocCrypto instance (from getDocBridge().getDocCrypto()).
|
||||
*/
|
||||
export function getVaultManager(docCrypto: DocCrypto): VaultManager {
|
||||
if (!vaultInstance) {
|
||||
vaultInstance = new VaultManager(docCrypto);
|
||||
}
|
||||
return vaultInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the vault manager (on logout).
|
||||
*/
|
||||
export function resetVaultManager(): void {
|
||||
if (vaultInstance) {
|
||||
vaultInstance.clear();
|
||||
vaultInstance = null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue