2 Commits

Author SHA1 Message Date
d64a6557a8 The vault notification mode1 2026-01-31 22:58:51 -04:00
56bb72aab8 The vault notification mode1 2026-01-30 22:42:20 -04:00
7 changed files with 991 additions and 350 deletions

View File

@@ -6,6 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Buffer } from 'buffer';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -15,6 +16,10 @@ import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext'; import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors'; import { colors } from './src/theme/colors';
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
globalThis.Buffer = Buffer;
}
/** /**
* Loading screen shown while restoring auth state * Loading screen shown while restoring auth state
*/ */

55
package-lock.json generated
View File

@@ -14,6 +14,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
@@ -3180,6 +3182,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4420,6 +4434,15 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/bip39": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
"license": "ISC",
"dependencies": {
"@noble/hashes": "^1.2.0"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -4507,9 +4530,9 @@
} }
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4527,7 +4550,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.1.13" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-alloc": { "node_modules/buffer-alloc": {
@@ -11035,6 +11058,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/whatwg-url-without-unicode/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",

View File

@@ -15,6 +15,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",

View File

@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
USER: { USER: {
id: 999, id: 999,
username: 'MockCaptain', username: 'MockCaptain',
email: 'captain@sentinel.local',
public_key: 'mock_public_key', public_key: 'mock_public_key',
is_admin: true, is_admin: true,
guale: false, guale: false,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@@ -8,47 +8,13 @@ import {
SafeAreaView, SafeAreaView,
Animated, Animated,
Modal, Modal,
TextInput,
KeyboardAvoidingView,
Platform,
Share,
Alert,
Linking,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types'; import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen'; import VaultScreen from './VaultScreen';
const MNEMONIC_WORDS = [
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
'salt', 'wave', 'grotto', 'lagoon', 'storm', 'north', 'south', 'east',
'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath',
];
const generateMnemonic = (wordCount = 12) => {
const words: string[] = [];
for (let i = 0; i < wordCount; i += 1) {
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
words.push(MNEMONIC_WORDS[index]);
}
return words;
};
const splitMnemonic = (words: string[]) => [
words.slice(0, 4),
words.slice(4, 8),
words.slice(8, 12),
];
// Status configuration with nautical theme // Status configuration with nautical theme
const statusConfig: Record<SystemStatus, { const statusConfig: Record<SystemStatus, {
color: string; color: string;
@@ -113,13 +79,6 @@ export default function SentinelScreen() {
const [glowAnim] = useState(new Animated.Value(0.5)); const [glowAnim] = useState(new Animated.Value(0.5));
const [rotateAnim] = useState(new Animated.Value(0)); const [rotateAnim] = useState(new Animated.Value(0));
const [showVault, setShowVault] = useState(false); const [showVault, setShowVault] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
const [showEmailForm, setShowEmailForm] = useState(false);
const [emailAddress, setEmailAddress] = useState('');
const [isCapturing, setIsCapturing] = useState(false);
const mnemonicRef = useRef<View>(null);
useEffect(() => { useEffect(() => {
// Pulse animation // Pulse animation
@@ -164,65 +123,8 @@ export default function SentinelScreen() {
).start(); ).start();
}, []); }, []);
const openVaultWithMnemonic = () => { const openVault = () => {
const words = generateMnemonic();
const parts = splitMnemonic(words);
setMnemonicWords(words);
setMnemonicParts(parts);
setShowMnemonic(true);
setShowVault(false);
setShowEmailForm(false);
setEmailAddress('');
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
// Best-effort local store; UI remains available
});
};
const handleScreenshot = async () => {
try {
setIsCapturing(true);
const uri = await captureRef(mnemonicRef, {
format: 'png',
quality: 1,
result: 'tmpfile',
});
await Share.share({
url: uri,
message: 'Sentinel key backup',
});
setShowMnemonic(false);
setShowVault(true); setShowVault(true);
} catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
} finally {
setIsCapturing(false);
}
};
const handleEmailBackup = () => {
setShowEmailForm(true);
};
const handleSendEmail = async () => {
const trimmed = emailAddress.trim();
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
Alert.alert('Invalid email', 'Please enter a valid email address.');
return;
}
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
try {
await Linking.openURL(mailtoUrl);
setShowMnemonic(false);
setShowEmailForm(false);
setEmailAddress('');
setShowVault(true);
} catch (error) {
Alert.alert('Email failed', 'Unable to open email client.');
}
}; };
const handleHeartbeat = () => { const handleHeartbeat = () => {
@@ -388,7 +290,7 @@ export default function SentinelScreen() {
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.vaultAccessButton} style={styles.vaultAccessButton}
onPress={openVaultWithMnemonic} onPress={openVault}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text style={styles.vaultAccessButtonText}>Open</Text> <Text style={styles.vaultAccessButtonText}>Open</Text>
@@ -446,7 +348,7 @@ export default function SentinelScreen() {
onRequestClose={() => setShowVault(false)} onRequestClose={() => setShowVault(false)}
> >
<View style={styles.vaultModalContainer}> <View style={styles.vaultModalContainer}>
<VaultScreen /> {showVault ? <VaultScreen /> : null}
<TouchableOpacity <TouchableOpacity
style={styles.vaultCloseButton} style={styles.vaultCloseButton}
onPress={() => setShowVault(false)} onPress={() => setShowVault(false)}
@@ -457,99 +359,6 @@ export default function SentinelScreen() {
</View> </View>
</Modal> </Modal>
{/* Mnemonic Modal */}
<Modal
visible={showMnemonic}
animationType="fade"
transparent
onRequestClose={() => setShowMnemonic(false)}
>
<KeyboardAvoidingView
style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<LinearGradient
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
style={styles.mnemonicCard}
ref={mnemonicRef}
>
<TouchableOpacity
style={styles.mnemonicClose}
onPress={() => setShowMnemonic(false)}
activeOpacity={0.85}
>
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
</TouchableOpacity>
<View style={styles.mnemonicHeader}>
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
</View>
<Text style={styles.mnemonicSubtitle}>
Your mnemonic is split into 3 parts (4/4/4). Part 1 is stored locally.
</Text>
<View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}>
{mnemonicWords.join(' ')}
</Text>
</View>
<View style={styles.partGrid}>
<View style={[styles.partCard, styles.partCardStored]}>
<Text style={styles.partLabel}>PART 1 LOCAL</Text>
<Text style={styles.partValue}>{mnemonicParts[0]?.join(' ')}</Text>
<Text style={styles.partHint}>Stored on this device</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>PART 2 CLOUD NODE</Text>
<Text style={styles.partValue}>{mnemonicParts[1]?.join(' ')}</Text>
<Text style={styles.partHint}>To be synced</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>PART 3 HEIR</Text>
<Text style={styles.partValue}>{mnemonicParts[2]?.join(' ')}</Text>
<Text style={styles.partHint}>To be assigned</Text>
</View>
</View>
<TouchableOpacity
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
onPress={handleScreenshot}
activeOpacity={0.85}
disabled={isCapturing}
>
<Text style={styles.mnemonicPrimaryText}>
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={handleEmailBackup}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
</TouchableOpacity>
{showEmailForm ? (
<View style={styles.emailForm}>
<TextInput
style={styles.emailInput}
value={emailAddress}
onChangeText={setEmailAddress}
placeholder="you@email.com"
placeholderTextColor={colors.sentinel.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.emailSendButton}
onPress={handleSendEmail}
activeOpacity={0.85}
>
<Text style={styles.emailSendText}>SEND EMAIL</Text>
</TouchableOpacity>
</View>
) : null}
</LinearGradient>
</KeyboardAvoidingView>
</Modal>
</View> </View>
); );
} }
@@ -810,148 +619,4 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
mnemonicOverlay: {
flex: 1,
backgroundColor: 'rgba(11, 20, 24, 0.72)',
justifyContent: 'center',
padding: spacing.lg,
},
mnemonicCard: {
borderRadius: borderRadius.xl,
padding: spacing.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
...shadows.glow,
},
mnemonicHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
mnemonicClose: {
position: 'absolute',
top: spacing.sm,
right: spacing.sm,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(26, 58, 74, 0.35)',
},
mnemonicTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.sentinel.text,
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSubtitle: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.textSecondary,
marginBottom: spacing.md,
},
mnemonicBlock: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.lg,
},
partGrid: {
gap: spacing.sm,
marginBottom: spacing.lg,
},
partCard: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
partCardStored: {
borderColor: colors.sentinel.primary,
},
partLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginBottom: 4,
fontWeight: '600',
},
partValue: {
fontSize: typography.fontSize.md,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '700',
marginBottom: 2,
},
partHint: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
},
mnemonicBlockText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '600',
lineHeight: 22,
textAlign: 'center',
},
mnemonicPrimaryButton: {
backgroundColor: colors.sentinel.primary,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
marginBottom: spacing.sm,
},
mnemonicButtonDisabled: {
opacity: 0.6,
},
mnemonicPrimaryText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSecondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
mnemonicSecondaryText: {
color: colors.sentinel.text,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
emailForm: {
marginTop: spacing.sm,
},
emailInput: {
height: 44,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
paddingHorizontal: spacing.md,
color: colors.sentinel.text,
fontSize: typography.fontSize.sm,
backgroundColor: 'rgba(255, 255, 255, 0.02)',
marginBottom: spacing.sm,
},
emailSendButton: {
backgroundColor: colors.nautical.teal,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
},
emailSendText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -77,6 +77,7 @@ export interface ProtocolInfo {
export interface User { export interface User {
id: number; id: number;
username: string; username: string;
email?: string;
public_key: string; public_key: string;
is_admin: boolean; is_admin: boolean;
guale: boolean; guale: boolean;