mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
fix runtime bug
This commit is contained in:
@@ -21,7 +21,9 @@
|
||||
"expo:dep:check": "expo install --fix",
|
||||
"expo:doctor": "pnpm dlx expo-doctor@latest",
|
||||
"test:e2e": "maestro test test/e2e/",
|
||||
"adb:logs": "while ! adb logcat --pid=$(adb shell pidof -s dev.fressh.app); do sleep 1; done"
|
||||
"adb:logs": "while ! adb logcat --pid=$(adb shell pidof -s dev.fressh.app); do sleep 1; done",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
|
||||
@@ -683,13 +683,10 @@ function ConnectionRow(props: {
|
||||
}
|
||||
// Recreate under new id then delete old
|
||||
await secretsManager.connections.utils.upsertConnection({
|
||||
id: newId,
|
||||
details,
|
||||
priority: 0,
|
||||
label: newId,
|
||||
});
|
||||
await secretsManager.connections.utils.deleteConnection(
|
||||
props.id,
|
||||
);
|
||||
await listQuery.refetch();
|
||||
setRenameOpen(false);
|
||||
}}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { preferences } from '@/lib/preferences';
|
||||
import {} from '@/lib/query-fns';
|
||||
import { useSshStore } from '@/lib/ssh-store';
|
||||
@@ -31,7 +32,9 @@ export default function TabsShellList() {
|
||||
}
|
||||
|
||||
function ShellContent() {
|
||||
const connections = useSshStore((s) => Object.values(s.connections));
|
||||
const connections = useSshStore(
|
||||
useShallow((s) => Object.values(s.connections)),
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -92,7 +95,7 @@ function FlatView({
|
||||
}: {
|
||||
setActionTarget: (target: ActionTarget) => void;
|
||||
}) {
|
||||
const shells = useSshStore((s) => Object.values(s.shells));
|
||||
const shells = useSshStore(useShallow((s) => Object.values(s.shells)));
|
||||
|
||||
return (
|
||||
<FlashList<SshShell>
|
||||
@@ -125,8 +128,10 @@ function GroupedView({
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
||||
const connections = useSshStore((s) => Object.values(s.connections));
|
||||
const shells = useSshStore((s) => Object.values(s.shells));
|
||||
const connections = useSshStore(
|
||||
useShallow((s) => Object.values(s.connections)),
|
||||
);
|
||||
const shells = useSshStore(useShallow((s) => Object.values(s.shells)));
|
||||
return (
|
||||
<FlashList<SshConnection>
|
||||
data={connections}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RnRussh } from '@fressh/react-native-uniffi-russh';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import * as Crypto from 'expo-crypto';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import React from 'react';
|
||||
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
||||
@@ -17,13 +17,8 @@ export function KeyList(props: {
|
||||
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const id = `key_${Date.now()}`;
|
||||
const pair = await secretsManager.keys.utils.generateKeyPair({
|
||||
type: 'rsa',
|
||||
keySize: 4096,
|
||||
});
|
||||
const pair = await RnRussh.generateKeyPair('ed25519');
|
||||
await secretsManager.keys.utils.upsertPrivateKey({
|
||||
keyId: id,
|
||||
metadata: { priority: 0, label: 'New Key', isDefault: false },
|
||||
value: pair,
|
||||
});
|
||||
@@ -98,9 +93,7 @@ function ImportKeyCard({ onImported }: { onImported: () => void }) {
|
||||
mutationFn: async () => {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) throw new Error('No key content provided');
|
||||
const keyId = `key_${Crypto.randomUUID()}`;
|
||||
await secretsManager.keys.utils.upsertPrivateKey({
|
||||
keyId,
|
||||
metadata: {
|
||||
priority: 0,
|
||||
label: label || 'Imported Key',
|
||||
|
||||
@@ -42,7 +42,7 @@ export const useSshConnMutation = (opts?: {
|
||||
});
|
||||
|
||||
await secretsManager.connections.utils.upsertConnection({
|
||||
id: sshConnection.connectionId,
|
||||
label: `${connectionDetails.username}@${connectionDetails.host}:${connectionDetails.port}`,
|
||||
details: connectionDetails,
|
||||
priority: 0,
|
||||
});
|
||||
|
||||
@@ -290,6 +290,7 @@ function makeBetterSecureStore<
|
||||
}),
|
||||
...valueChunks.map(async (vChunk, chunkIdx) => {
|
||||
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
||||
console.log('DEBUG: setting entry chunk', entryKeyString);
|
||||
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
||||
log(
|
||||
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
||||
@@ -325,20 +326,23 @@ const betterKeyStorage = makeBetterSecureStore<KeyMetadata>({
|
||||
});
|
||||
|
||||
async function upsertPrivateKey(params: {
|
||||
keyId: string;
|
||||
keyId?: string;
|
||||
metadata: StrictOmit<KeyMetadata, 'createdAtMs'>;
|
||||
value: string;
|
||||
}) {
|
||||
log(`Upserting private key ${params.keyId}`);
|
||||
const validKey = RnRussh.validatePrivateKey(params.value);
|
||||
if (!validKey) throw new Error('Invalid private key');
|
||||
const keyId = params.keyId ?? `key_${Crypto.randomUUID()}`;
|
||||
log(`${params.keyId ? 'Upserting' : 'Creating'} private key ${keyId}`);
|
||||
// Preserve createdAtMs if the entry already exists
|
||||
const existing = await betterKeyStorage
|
||||
.getEntry(params.keyId)
|
||||
.getEntry(keyId)
|
||||
.catch(() => undefined);
|
||||
const createdAtMs =
|
||||
existing?.manifestEntry.metadata.createdAtMs ?? Date.now();
|
||||
|
||||
await betterKeyStorage.upsertEntry({
|
||||
id: params.keyId,
|
||||
id: keyId,
|
||||
metadata: {
|
||||
...params.metadata,
|
||||
createdAtMs,
|
||||
@@ -393,6 +397,7 @@ const betterConnectionStorage = makeBetterSecureStore({
|
||||
priority: z.number(),
|
||||
createdAtMs: z.int(),
|
||||
modifiedAtMs: z.int(),
|
||||
label: z.string().optional(),
|
||||
}),
|
||||
parseValue: (value) => connectionDetailsSchema.parse(JSON.parse(value)),
|
||||
});
|
||||
@@ -400,16 +405,18 @@ const betterConnectionStorage = makeBetterSecureStore({
|
||||
export type InputConnectionDetails = z.infer<typeof connectionDetailsSchema>;
|
||||
|
||||
async function upsertConnection(params: {
|
||||
id: string;
|
||||
details: InputConnectionDetails;
|
||||
priority: number;
|
||||
label?: string;
|
||||
}) {
|
||||
const id = `${params.details.username}-${params.details.host}-${params.details.port}`.replaceAll('.', '_');
|
||||
await betterConnectionStorage.upsertEntry({
|
||||
id: params.id,
|
||||
id,
|
||||
metadata: {
|
||||
priority: params.priority,
|
||||
createdAtMs: Date.now(),
|
||||
modifiedAtMs: Date.now(),
|
||||
createdAtMs: Date.now(),
|
||||
label: params.label,
|
||||
},
|
||||
value: JSON.stringify(params.details),
|
||||
});
|
||||
@@ -436,32 +443,13 @@ const getConnectionQueryOptions = (id: string) =>
|
||||
queryFn: () => betterConnectionStorage.getEntry(id),
|
||||
});
|
||||
|
||||
// https://github.com/dylankenneally/react-native-ssh-sftp/blob/ea55436d8d40378a8f9dabb95b463739ffb219fa/android/src/main/java/me/keeex/rnssh/RNSshClientModule.java#L101-L119
|
||||
export type SshPrivateKeyType = 'dsa' | 'rsa' | 'ecdsa' | 'ed25519' | 'ed448';
|
||||
async function generateKeyPair(params: {
|
||||
type: SshPrivateKeyType;
|
||||
passphrase?: string;
|
||||
keySize?: number;
|
||||
comment?: string;
|
||||
}) {
|
||||
log('DEBUG: generating key pair', params);
|
||||
const keyPair = await RnRussh.generateKeyPair(
|
||||
'ed25519',
|
||||
// params.keySize,
|
||||
// params.comment ?? '',
|
||||
);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
export const secretsManager = {
|
||||
keys: {
|
||||
utils: {
|
||||
upsertPrivateKey,
|
||||
deletePrivateKey,
|
||||
generateKeyPair,
|
||||
listEntriesWithValues: betterKeyStorage.listEntriesWithValues,
|
||||
getPrivateKey: (keyId: string) => betterKeyStorage.getEntry(keyId),
|
||||
// Intentionally no specialized setters; use upsertPrivateKey instead.
|
||||
},
|
||||
query: {
|
||||
list: listKeysQueryOptions,
|
||||
|
||||
@@ -147,7 +147,13 @@ export type SshShell = {
|
||||
type RusshApi = {
|
||||
uniffiInitAsync: () => Promise<void>;
|
||||
connect: (opts: ConnectOptions) => Promise<SshConnection>;
|
||||
generateKeyPair: (type: 'rsa' | 'ecdsa' | 'ed25519') => Promise<string>;
|
||||
generateKeyPair: (type: 'rsa' | 'ecdsa' | 'ed25519',
|
||||
// TODO: Add these
|
||||
// passphrase?: string;
|
||||
// keySize?: number;
|
||||
// comment?: string;
|
||||
) => Promise<string>;
|
||||
validatePrivateKey: (key: string) => boolean;
|
||||
};
|
||||
|
||||
// #endregion
|
||||
@@ -373,10 +379,20 @@ async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') {
|
||||
return GeneratedRussh.generateKeyPair(map[type]);
|
||||
}
|
||||
|
||||
function validatePrivateKey(key: string) {
|
||||
try {
|
||||
GeneratedRussh.validatePrivateKey(key);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
export const RnRussh = {
|
||||
uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
|
||||
connect,
|
||||
generateKeyPair,
|
||||
validatePrivateKey,
|
||||
} satisfies RusshApi;
|
||||
|
||||
@@ -8,7 +8,9 @@ const logExternal: boolean = false;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
react({
|
||||
|
||||
}),
|
||||
dts({
|
||||
tsconfigPath: './tsconfig.app.json',
|
||||
// This makes dist/ look nice but breaks Cmd + Click
|
||||
|
||||
Reference in New Issue
Block a user