Files
Shade/packages/shade-recovery/src/store.ts

92 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* Guardian-side persistence for received shares.
*
* Each guardian holds, per protected user, exactly one record per
* `setupId`:
*
* {
* originalAddress, // the protected user's Shade address
* setupId, // disambiguates if a user re-runs setup
* shareIndex, // their assigned x-coordinate
* shareBytes, // base64-encoded Shamir share
* shareSecretCiphertext, // base64 — encrypted backup payload
* shareSecretNonce, // base64 — AES-GCM nonce
* setupFingerprint, // safety number at deposit time
* guardianCount, threshold, // informational
* receivedAt
* }
*
* The interface is deliberately minimal so consumers can back it with
* whatever they already use for app state IndexedDB, AsyncStorage,
* SQLite, etc. The package ships an in-memory implementation suitable
* for tests and small one-device demos. Guardian apps that survive
* restart MUST supply a persistent implementation.
*/
export interface GuardianShareEntry {
originalAddress: string;
setupId: string;
shareIndex: number;
shareBytes: string;
shareSecretCiphertext: string;
shareSecretNonce: string;
setupFingerprint: string;
guardianCount: number;
threshold: number;
receivedAt: number;
}
/**
* The minimal contract a guardian-side share store must implement.
*
* Shapes:
* - `save` is upsert by (originalAddress, setupId). If a user re-runs
* setup with a different setupId, both entries persist; the guardian
* can choose which to release based on the recovery-request's
* setupId. If the same (originalAddress, setupId) is saved twice,
* the second write wins (idempotent re-deposit).
* - `get` returns the deposit for the (originalAddress, setupId) pair,
* or `null` when none exists.
* - `list` enumerates everything (used by guardian-UX widgets).
* - `delete` removes a single deposit; the guardian-UX exposes this so
* a user can prune deposits from people they no longer wish to
* protect.
*/
export interface RecoveryStore {
save(entry: GuardianShareEntry): Promise<void>;
get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null>;
list(): Promise<GuardianShareEntry[]>;
delete(originalAddress: string, setupId: string): Promise<void>;
}
/**
* Process-local in-memory store. Suitable for tests and one-shot demos;
* LOSES STATE ON RESTART. Production guardian apps must supply a
* persistent `RecoveryStore` (see `docs/recovery.md` for backing-store
* recommendations).
*/
export class MemoryRecoveryStore implements RecoveryStore {
private readonly entries = new Map<string, GuardianShareEntry>();
async save(entry: GuardianShareEntry): Promise<void> {
this.entries.set(keyOf(entry.originalAddress, entry.setupId), { ...entry });
}
async get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null> {
const found = this.entries.get(keyOf(originalAddress, setupId));
return found === undefined ? null : { ...found };
}
async list(): Promise<GuardianShareEntry[]> {
return Array.from(this.entries.values()).map((e) => ({ ...e }));
}
async delete(originalAddress: string, setupId: string): Promise<void> {
this.entries.delete(keyOf(originalAddress, setupId));
}
}
function keyOf(originalAddress: string, setupId: string): string {
return `${originalAddress} ${setupId}`;
}