added_reveal_secret_and_delete_treasure

This commit is contained in:
lusixing
2026-02-02 17:34:03 -08:00
parent b5373c2d9a
commit 5c1172a912
8 changed files with 377 additions and 84 deletions

View File

@@ -51,6 +51,7 @@ export const API_ENDPOINTS = {
CREATE: '/assets/create',
CLAIM: '/assets/claim',
ASSIGN: '/assets/assign',
DELETE: '/assets/delete',
},
// AI Services

View File

@@ -11,6 +11,7 @@ import { useAuth } from '../context/AuthContext';
import { assetsService } from '../services/assets.service';
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
import { SentinelVault } from '../utils/crypto_core';
import { storageService } from '../services/storage.service';
import {
initialVaultAssets,
mapApiAssetsToVaultAssets,
@@ -37,6 +38,8 @@ export interface UseVaultAssetsReturn {
refreshAssets: () => Promise<void>;
/** Create asset via POST /assets/create; on success refreshes list */
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
/** Delete asset via POST /assets/delete; on success refreshes list */
deleteAsset: (assetId: number) => Promise<CreateAssetResult>;
/** True while create request is in flight */
isSealing: boolean;
/** Error message from last create failure (non-401) */
@@ -129,7 +132,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
console.log(' Encrypted: ', content_inner_encrypted);
}
await assetsService.createAsset(
const createdAsset = await assetsService.createAsset(
{
title: title.trim(),
private_key_shard: s1Str,
@@ -137,6 +140,11 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
},
token
);
// Backup plaintext content locally
if (createdAsset && createdAsset.id && user?.id) {
await storageService.saveAssetBackup(createdAsset.id, content, user.id);
}
await refreshAssets();
return { success: true };
} catch (err: unknown) {
@@ -166,6 +174,44 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
[token, user, refreshAssets, signOut]
);
const deleteAsset = useCallback(
async (assetId: number): Promise<CreateAssetResult> => {
if (!token) {
return { success: false, error: 'Not logged in.' };
}
setIsSealing(true);
setCreateError(null);
try {
await assetsService.deleteAsset(assetId, token);
await refreshAssets();
return { success: true };
} catch (err: unknown) {
const status =
err && typeof err === 'object' && 'status' in err
? (err as { status?: number }).status
: undefined;
const rawMessage =
err instanceof Error ? err.message : String(err ?? 'Failed to delete.');
const isUnauthorized =
status === 401 || /401|Unauthorized/i.test(rawMessage);
if (isUnauthorized) {
signOut();
return { success: false, isUnauthorized: true };
}
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
? 'Network error. Please check that the backend is running and reachable.'
: rawMessage;
setCreateError(friendlyMessage);
return { success: false, error: friendlyMessage };
} finally {
setIsSealing(false);
}
},
[token, refreshAssets, signOut]
);
const clearCreateError = useCallback(() => setCreateError(null), []);
return {
@@ -173,6 +219,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
setAssets,
refreshAssets,
createAsset,
deleteAsset,
isSealing,
createError,
clearCreateError,

View File

@@ -542,12 +542,17 @@ export default function FlowScreen() {
const encryptedSummary = vault.encryptData(aesKey, generatedSummary).toString('hex');
// Create asset in backend
await assetsService.createAsset({
const createdAsset = await assetsService.createAsset({
title: `Chat Summary - ${new Date().toLocaleDateString()}`,
private_key_shard: shareServer,
content_inner_encrypted: encryptedSummary,
}, token);
// Backup plaintext content locally
if (createdAsset && createdAsset.id && user?.id) {
await storageService.saveAssetBackup(createdAsset.id, generatedSummary, user.id);
}
setSaveResult({ success: true, message: 'Summary encrypted and saved to your vault successfully.' });
setShowSaveResultModal(true);
} catch (error) {

View File

@@ -27,6 +27,7 @@ import { useAuth } from '../context/AuthContext';
import { useVaultAssets } from '../hooks/useVaultAssets';
import { getVaultStorageKeys } from '../config';
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
import { storageService } from '../services/storage.service';
import { SentinelVault } from '@/utils/crypto_core';
// Asset type configuration with nautical theme
@@ -92,41 +93,6 @@ type HeirAssignment = {
heir: Heir;
};
// Mock data
// const initialAssets: VaultAsset[] = [
// {
// id: '1',
// type: 'private_key',
// label: 'ETH Main Wallet Key',
// createdAt: new Date('2024-01-10'),
// updatedAt: new Date('2024-01-10'),
// isEncrypted: true,
// },
// {
// id: '2',
// type: 'game_account',
// label: 'Steam Account Credentials',
// createdAt: new Date('2024-01-08'),
// updatedAt: new Date('2024-01-08'),
// isEncrypted: true,
// },
// {
// id: '3',
// type: 'document',
// label: 'Insurance Policy Scan',
// createdAt: new Date('2024-01-05'),
// updatedAt: new Date('2024-01-05'),
// isEncrypted: true,
// },
// {
// id: '4',
// type: 'will',
// label: 'Testament Draft v2',
// createdAt: new Date('2024-01-02'),
// updatedAt: new Date('2024-01-15'),
// isEncrypted: true,
// },
// ];
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
switch (config.iconType) {
@@ -149,11 +115,14 @@ export default function VaultScreen() {
setAssets,
refreshAssets,
createAsset: createVaultAsset,
deleteAsset: deleteVaultAsset,
isSealing,
createError: addError,
clearCreateError: clearAddError,
} = useVaultAssets(isUnlocked);
const [showAddModal, setShowAddModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
const [newLabel, setNewLabel] = useState('');
const [showUploadSuccess, setShowUploadSuccess] = useState(false);
@@ -172,6 +141,8 @@ export default function VaultScreen() {
const [showMnemonic, setShowMnemonic] = useState(false);
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
const [hasS0, setHasS0] = useState<boolean | null>(null);
const [backupContent, setBackupContent] = useState<string | null>(null);
const [isFetchingBackup, setIsFetchingBackup] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
@@ -504,6 +475,61 @@ export default function VaultScreen() {
setSelectedAsset(null);
setShowKeyPreview(false);
setShowGuardedBiometric(false);
setBackupContent(null);
};
const handleFetchBackup = async () => {
if (!selectedAsset || !user?.id) return;
setIsFetchingBackup(true);
try {
const content = await storageService.getAssetBackup(Number(selectedAsset.id), user.id);
if (content) {
setBackupContent(content);
} else {
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('No Backup Found', 'No local plaintext backup found for this treasure.');
}
}
} catch (error) {
console.error('Fetch backup error:', error);
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Error', 'Failed to retrieve local backup.');
}
} finally {
setIsFetchingBackup(false);
}
};
const handleDeleteAsset = async () => {
if (!selectedAsset || isDeleting) return;
setIsDeleting(true);
try {
const result = await deleteVaultAsset(Number(selectedAsset.id));
if (result.success) {
setShowDeleteConfirm(false);
handleCloseDetail();
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Success', 'Treasure removed from the vault.');
}
} else if (result.isUnauthorized) {
setShowDeleteConfirm(false);
handleCloseDetail();
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
}
} else if (result.error && typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Failed', result.error);
}
} catch (error) {
console.error('Delete error:', error);
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Error', 'An unexpected error occurred during deletion.');
}
} finally {
setIsDeleting(false);
}
};
const handleGuardedAccess = () => {
@@ -513,6 +539,7 @@ export default function VaultScreen() {
const handleGuardedSuccess = () => {
setShowGuardedBiometric(false);
setShowKeyPreview(true);
handleFetchBackup();
};
const handleAddVerification = () => {
@@ -1464,6 +1491,14 @@ export default function VaultScreen() {
<MaterialCommunityIcons name="refresh" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionRow, styles.deleteActionRow]}
onPress={() => setShowDeleteConfirm(true)}
activeOpacity={0.8}
>
<Feather name="trash-2" size={18} color={colors.vault.warning} />
<Text style={[styles.actionText, styles.deleteActionText]}>Delete Treasure</Text>
</TouchableOpacity>
</View>
<View style={styles.guardCard}>
@@ -1474,6 +1509,7 @@ export default function VaultScreen() {
<Text style={styles.guardText}>
Plaintext access requires biometric verification and a memory rehearsal step.
</Text>
<TouchableOpacity
style={styles.guardButton}
onPress={handleGuardedAccess}
@@ -1481,10 +1517,13 @@ export default function VaultScreen() {
>
<Text style={styles.guardButtonText}>Begin Verification</Text>
</TouchableOpacity>
{showKeyPreview && (
<View style={styles.previewCard}>
<Text style={styles.previewLabel}>MNEMONIC SHARD (MASKED)</Text>
<Text style={styles.previewValue}>ocean-anchored-ember-veil</Text>
<Text style={styles.previewLabel}>LOCAL PLAINTEXT BACKUP</Text>
<Text style={styles.previewValue}>
{isFetchingBackup ? 'Fetching content...' : (backupContent || 'No local backup found for this treasure')}
</Text>
</View>
)}
</View>
@@ -1509,6 +1548,51 @@ export default function VaultScreen() {
isDark
/>
</Modal>
{/* Delete Confirmation Modal */}
<Modal
visible={showDeleteConfirm}
animationType="fade"
transparent
onRequestClose={() => setShowDeleteConfirm(false)}
>
<View style={styles.modalOverlay}>
<TouchableOpacity
style={styles.modalOverlayDismiss}
activeOpacity={1}
onPress={() => setShowDeleteConfirm(false)}
/>
<View style={styles.deleteConfirmContent}>
<View style={styles.deleteIconContainer}>
<Feather name="alert-triangle" size={32} color={colors.vault.warning} />
</View>
<Text style={styles.deleteTitle}>Remove Treasure?</Text>
<Text style={styles.deleteMessage}>
This action cannot be undone. The treasure will be permanently shredded from the deep vault.
</Text>
<View style={styles.deleteButtons}>
<TouchableOpacity
style={styles.deleteCancelButton}
onPress={() => setShowDeleteConfirm(false)}
disabled={isDeleting}
>
<Text style={styles.deleteCancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteConfirmButton}
onPress={handleDeleteAsset}
disabled={isDeleting}
>
{isDeleting ? (
<Text style={styles.deleteConfirmText}>Shredding...</Text>
) : (
<Text style={styles.deleteConfirmText}>Confirm Delete</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
);
@@ -1777,6 +1861,73 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(26, 58, 74, 0.8)',
justifyContent: 'flex-end',
},
modalOverlayDismiss: {
...StyleSheet.absoluteFillObject,
},
deleteConfirmContent: {
backgroundColor: colors.vault.cardBackground,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.xl,
padding: spacing.xl,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
alignItems: 'center',
marginBottom: spacing.xxl + spacing.lg,
},
deleteIconContainer: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: `${colors.vault.warning}20`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
},
deleteTitle: {
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
marginBottom: spacing.sm,
fontFamily: typography.fontFamily.serif,
},
deleteMessage: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.sm * 1.5,
marginBottom: spacing.xl,
},
deleteButtons: {
flexDirection: 'row',
gap: spacing.md,
width: '100%',
},
deleteCancelButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
deleteCancelText: {
color: colors.vault.textSecondary,
fontSize: typography.fontSize.base,
fontWeight: '600',
},
deleteConfirmButton: {
flex: 2,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.vault.warning,
alignItems: 'center',
},
deleteConfirmText: {
color: colors.vault.text,
fontSize: typography.fontSize.base,
fontWeight: '700',
},
modalContent: {
backgroundColor: colors.nautical.cream,
borderTopLeftRadius: borderRadius.xxl,
@@ -2581,8 +2732,66 @@ const styles = StyleSheet.create({
borderColor: colors.sentinel.cardBorder,
},
mnemonicSecondaryText: {
color: colors.sentinel.text,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
deleteActionRow: {
backgroundColor: 'rgba(229, 115, 115, 0.08)',
borderColor: 'rgba(229, 115, 115, 0.2)',
marginTop: spacing.sm,
},
deleteActionText: {
color: colors.vault.warning,
},
confirmModalOverlay: {
flex: 1,
backgroundColor: 'rgba(11, 20, 24, 0.85)',
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
},
confirmModalContent: {
width: '100%',
backgroundColor: colors.vault.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.lg,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
gap: spacing.md,
},
confirmModalTitle: {
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
textAlign: 'center',
},
confirmModalText: {
fontSize: typography.fontSize.base,
color: colors.vault.textSecondary,
textAlign: 'center',
lineHeight: 22,
},
confirmModalButtons: {
flexDirection: 'row',
gap: spacing.md,
marginTop: spacing.sm,
},
confirmModalButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
},
confirmCancelButton: {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
confirmDeleteButton: {
backgroundColor: colors.vault.warning,
},
confirmCancelText: {
color: colors.vault.textSecondary,
},
});

View File

@@ -245,4 +245,44 @@ export const assetsService = {
throw error;
}
},
/**
* Delete an asset
* @param assetId - ID of the asset to delete
* @param token - JWT token for authentication
* @returns Success message
*/
async deleteAsset(assetId: number, token: string): Promise<{ message: string }> {
if (NO_BACKEND_MODE) {
logApiDebug('Delete Asset', `Using mock mode for ID: ${assetId}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve({ message: 'Asset deleted successfully' });
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ASSETS.DELETE);
logApiDebug('Delete Asset URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify({ asset_id: assetId }),
});
logApiDebug('Delete Asset Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to delete asset');
}
return await response.json();
} catch (error) {
console.error('Delete asset error:', error);
throw error;
}
},
};

View File

@@ -14,6 +14,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEYS = {
CHAT_HISTORY: '@sentinel:chat_history',
CURRENT_MESSAGES: '@sentinel:current_messages',
ASSET_BACKUP: '@sentinel:asset_backup',
} as const;
// =============================================================================
@@ -115,6 +116,32 @@ export const storageService = {
} catch (e) {
console.error('Error clearing storage data:', e);
}
},
/**
* Save the plaintext backup of an asset locally
*/
async saveAssetBackup(assetId: number, content: string, userId: string | number): Promise<void> {
try {
const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`;
await AsyncStorage.setItem(key, content);
console.log(`[Storage] Saved asset backup for user ${userId}, asset ${assetId}`);
} catch (e) {
console.error(`Error saving asset backup for asset ${assetId}:`, e);
}
},
/**
* Retrieve the plaintext backup of an asset locally
*/
async getAssetBackup(assetId: number, userId: string | number): Promise<string | null> {
try {
const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`;
return await AsyncStorage.getItem(key);
} catch (e) {
console.error(`Error getting asset backup for asset ${assetId}:`, e);
return null;
}
}
};

View File

@@ -5,7 +5,7 @@ export const colors = {
// Base colors
white: '#FFFFFF',
black: '#1A2F3A',
// Nautical palette
nautical: {
deepTeal: '#1B4D5C',
@@ -21,7 +21,7 @@ export const colors = {
navy: '#1A3A4A',
sage: '#8CA5A5',
},
// Flow - Captain's Journal
flow: {
background: '#E8F6F8',
@@ -38,7 +38,7 @@ export const colors = {
archivedText: '#7A9A9A',
highlight: '#B8E0E5',
},
// Vault - Ship's Vault
vault: {
background: '#1B4D5C',
@@ -54,7 +54,7 @@ export const colors = {
warning: '#E57373',
success: '#6BBF8A',
},
// Sentinel - Lighthouse Watch
sentinel: {
background: '#1A3A4A',
@@ -70,7 +70,7 @@ export const colors = {
statusWarning: '#E5B873',
statusCritical: '#E57373',
},
// Heritage - Legacy Fleet
heritage: {
background: '#E8F6F8',
@@ -86,7 +86,7 @@ export const colors = {
confirmed: '#6BBF8A',
pending: '#E5B873',
},
// Me - Captain's Quarters
me: {
background: '#E8F6F8',

View File

@@ -31,6 +31,8 @@ export const VAULT_ASSET_TYPES: VaultAssetType[] = [
'custom',
];
export const initialVaultAssets: VaultAsset[] = [];
// -----------------------------------------------------------------------------
// Mapping
// -----------------------------------------------------------------------------
@@ -60,41 +62,3 @@ export function mapApiAssetsToVaultAssets(apiList: ApiAsset[]): VaultAsset[] {
return apiList.map(mapApiAssetToVaultAsset);
}
// -----------------------------------------------------------------------------
// Mock / initial data (fallback when API is unavailable)
// -----------------------------------------------------------------------------
export const initialVaultAssets: VaultAsset[] = [
{
id: '1',
type: 'private_key',
label: 'ETH Main Wallet Key',
createdAt: new Date('2024-01-10'),
updatedAt: new Date('2024-01-10'),
isEncrypted: true,
},
{
id: '2',
type: 'game_account',
label: 'Steam Account Credentials',
createdAt: new Date('2024-01-08'),
updatedAt: new Date('2024-01-08'),
isEncrypted: true,
},
{
id: '3',
type: 'document',
label: 'Insurance Policy Scan',
createdAt: new Date('2024-01-05'),
updatedAt: new Date('2024-01-05'),
isEncrypted: true,
},
{
id: '4',
type: 'will',
label: 'Testament Draft v2',
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-15'),
isEncrypted: true,
},
];