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:dep:check": "expo install --fix",
|
||||||
"expo:doctor": "pnpm dlx expo-doctor@latest",
|
"expo:doctor": "pnpm dlx expo-doctor@latest",
|
||||||
"test:e2e": "maestro test test/e2e/",
|
"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": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
|
|||||||
@@ -683,13 +683,10 @@ function ConnectionRow(props: {
|
|||||||
}
|
}
|
||||||
// Recreate under new id then delete old
|
// Recreate under new id then delete old
|
||||||
await secretsManager.connections.utils.upsertConnection({
|
await secretsManager.connections.utils.upsertConnection({
|
||||||
id: newId,
|
|
||||||
details,
|
details,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
label: newId,
|
||||||
});
|
});
|
||||||
await secretsManager.connections.utils.deleteConnection(
|
|
||||||
props.id,
|
|
||||||
);
|
|
||||||
await listQuery.refetch();
|
await listQuery.refetch();
|
||||||
setRenameOpen(false);
|
setRenameOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { preferences } from '@/lib/preferences';
|
import { preferences } from '@/lib/preferences';
|
||||||
import {} from '@/lib/query-fns';
|
import {} from '@/lib/query-fns';
|
||||||
import { useSshStore } from '@/lib/ssh-store';
|
import { useSshStore } from '@/lib/ssh-store';
|
||||||
@@ -31,7 +32,9 @@ export default function TabsShellList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ShellContent() {
|
function ShellContent() {
|
||||||
const connections = useSshStore((s) => Object.values(s.connections));
|
const connections = useSshStore(
|
||||||
|
useShallow((s) => Object.values(s.connections)),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
@@ -92,7 +95,7 @@ function FlatView({
|
|||||||
}: {
|
}: {
|
||||||
setActionTarget: (target: ActionTarget) => void;
|
setActionTarget: (target: ActionTarget) => void;
|
||||||
}) {
|
}) {
|
||||||
const shells = useSshStore((s) => Object.values(s.shells));
|
const shells = useSshStore(useShallow((s) => Object.values(s.shells)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList<SshShell>
|
<FlashList<SshShell>
|
||||||
@@ -125,8 +128,10 @@ function GroupedView({
|
|||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
||||||
const connections = useSshStore((s) => Object.values(s.connections));
|
const connections = useSshStore(
|
||||||
const shells = useSshStore((s) => Object.values(s.shells));
|
useShallow((s) => Object.values(s.connections)),
|
||||||
|
);
|
||||||
|
const shells = useSshStore(useShallow((s) => Object.values(s.shells)));
|
||||||
return (
|
return (
|
||||||
<FlashList<SshConnection>
|
<FlashList<SshConnection>
|
||||||
data={connections}
|
data={connections}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { RnRussh } from '@fressh/react-native-uniffi-russh';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import * as Crypto from 'expo-crypto';
|
|
||||||
import * as DocumentPicker from 'expo-document-picker';
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
||||||
@@ -17,13 +17,8 @@ export function KeyList(props: {
|
|||||||
|
|
||||||
const generateMutation = useMutation({
|
const generateMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const id = `key_${Date.now()}`;
|
const pair = await RnRussh.generateKeyPair('ed25519');
|
||||||
const pair = await secretsManager.keys.utils.generateKeyPair({
|
|
||||||
type: 'rsa',
|
|
||||||
keySize: 4096,
|
|
||||||
});
|
|
||||||
await secretsManager.keys.utils.upsertPrivateKey({
|
await secretsManager.keys.utils.upsertPrivateKey({
|
||||||
keyId: id,
|
|
||||||
metadata: { priority: 0, label: 'New Key', isDefault: false },
|
metadata: { priority: 0, label: 'New Key', isDefault: false },
|
||||||
value: pair,
|
value: pair,
|
||||||
});
|
});
|
||||||
@@ -98,9 +93,7 @@ function ImportKeyCard({ onImported }: { onImported: () => void }) {
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
if (!trimmed) throw new Error('No key content provided');
|
if (!trimmed) throw new Error('No key content provided');
|
||||||
const keyId = `key_${Crypto.randomUUID()}`;
|
|
||||||
await secretsManager.keys.utils.upsertPrivateKey({
|
await secretsManager.keys.utils.upsertPrivateKey({
|
||||||
keyId,
|
|
||||||
metadata: {
|
metadata: {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
label: label || 'Imported Key',
|
label: label || 'Imported Key',
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ export const useSshConnMutation = (opts?: {
|
|||||||
const security =
|
const security =
|
||||||
connectionDetails.security.type === 'password'
|
connectionDetails.security.type === 'password'
|
||||||
? {
|
? {
|
||||||
type: 'password' as const,
|
type: 'password' as const,
|
||||||
password: connectionDetails.security.password,
|
password: connectionDetails.security.password,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'key' as const,
|
type: 'key' as const,
|
||||||
privateKey: await secretsManager.keys.utils
|
privateKey: await secretsManager.keys.utils
|
||||||
.getPrivateKey(connectionDetails.security.keyId)
|
.getPrivateKey(connectionDetails.security.keyId)
|
||||||
.then((e) => e.value),
|
.then((e) => e.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshConnection = await connect({
|
const sshConnection = await connect({
|
||||||
host: connectionDetails.host,
|
host: connectionDetails.host,
|
||||||
@@ -42,7 +42,7 @@ export const useSshConnMutation = (opts?: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await secretsManager.connections.utils.upsertConnection({
|
await secretsManager.connections.utils.upsertConnection({
|
||||||
id: sshConnection.connectionId,
|
label: `${connectionDetails.username}@${connectionDetails.host}:${connectionDetails.port}`,
|
||||||
details: connectionDetails,
|
details: connectionDetails,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ function makeBetterSecureStore<
|
|||||||
const unsafedRootManifest: unknown = rawRootManifestString
|
const unsafedRootManifest: unknown = rawRootManifestString
|
||||||
? JSON.parse(rawRootManifestString)
|
? JSON.parse(rawRootManifestString)
|
||||||
: {
|
: {
|
||||||
manifestVersion: rootManifestVersion,
|
manifestVersion: rootManifestVersion,
|
||||||
manifestChunksIds: [],
|
manifestChunksIds: [],
|
||||||
};
|
};
|
||||||
const rootManifest = rootManifestSchema.parse(unsafedRootManifest);
|
const rootManifest = rootManifestSchema.parse(unsafedRootManifest);
|
||||||
const manifestChunks = await Promise.all(
|
const manifestChunks = await Promise.all(
|
||||||
rootManifest.manifestChunksIds.map(async (manifestChunkId) => {
|
rootManifest.manifestChunksIds.map(async (manifestChunkId) => {
|
||||||
@@ -290,6 +290,7 @@ function makeBetterSecureStore<
|
|||||||
}),
|
}),
|
||||||
...valueChunks.map(async (vChunk, chunkIdx) => {
|
...valueChunks.map(async (vChunk, chunkIdx) => {
|
||||||
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
||||||
|
console.log('DEBUG: setting entry chunk', entryKeyString);
|
||||||
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
||||||
log(
|
log(
|
||||||
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
||||||
@@ -325,20 +326,23 @@ const betterKeyStorage = makeBetterSecureStore<KeyMetadata>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function upsertPrivateKey(params: {
|
async function upsertPrivateKey(params: {
|
||||||
keyId: string;
|
keyId?: string;
|
||||||
metadata: StrictOmit<KeyMetadata, 'createdAtMs'>;
|
metadata: StrictOmit<KeyMetadata, 'createdAtMs'>;
|
||||||
value: string;
|
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
|
// Preserve createdAtMs if the entry already exists
|
||||||
const existing = await betterKeyStorage
|
const existing = await betterKeyStorage
|
||||||
.getEntry(params.keyId)
|
.getEntry(keyId)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
const createdAtMs =
|
const createdAtMs =
|
||||||
existing?.manifestEntry.metadata.createdAtMs ?? Date.now();
|
existing?.manifestEntry.metadata.createdAtMs ?? Date.now();
|
||||||
|
|
||||||
await betterKeyStorage.upsertEntry({
|
await betterKeyStorage.upsertEntry({
|
||||||
id: params.keyId,
|
id: keyId,
|
||||||
metadata: {
|
metadata: {
|
||||||
...params.metadata,
|
...params.metadata,
|
||||||
createdAtMs,
|
createdAtMs,
|
||||||
@@ -393,6 +397,7 @@ const betterConnectionStorage = makeBetterSecureStore({
|
|||||||
priority: z.number(),
|
priority: z.number(),
|
||||||
createdAtMs: z.int(),
|
createdAtMs: z.int(),
|
||||||
modifiedAtMs: z.int(),
|
modifiedAtMs: z.int(),
|
||||||
|
label: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
parseValue: (value) => connectionDetailsSchema.parse(JSON.parse(value)),
|
parseValue: (value) => connectionDetailsSchema.parse(JSON.parse(value)),
|
||||||
});
|
});
|
||||||
@@ -400,16 +405,18 @@ const betterConnectionStorage = makeBetterSecureStore({
|
|||||||
export type InputConnectionDetails = z.infer<typeof connectionDetailsSchema>;
|
export type InputConnectionDetails = z.infer<typeof connectionDetailsSchema>;
|
||||||
|
|
||||||
async function upsertConnection(params: {
|
async function upsertConnection(params: {
|
||||||
id: string;
|
|
||||||
details: InputConnectionDetails;
|
details: InputConnectionDetails;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
label?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const id = `${params.details.username}-${params.details.host}-${params.details.port}`.replaceAll('.', '_');
|
||||||
await betterConnectionStorage.upsertEntry({
|
await betterConnectionStorage.upsertEntry({
|
||||||
id: params.id,
|
id,
|
||||||
metadata: {
|
metadata: {
|
||||||
priority: params.priority,
|
priority: params.priority,
|
||||||
createdAtMs: Date.now(),
|
|
||||||
modifiedAtMs: Date.now(),
|
modifiedAtMs: Date.now(),
|
||||||
|
createdAtMs: Date.now(),
|
||||||
|
label: params.label,
|
||||||
},
|
},
|
||||||
value: JSON.stringify(params.details),
|
value: JSON.stringify(params.details),
|
||||||
});
|
});
|
||||||
@@ -436,32 +443,13 @@ const getConnectionQueryOptions = (id: string) =>
|
|||||||
queryFn: () => betterConnectionStorage.getEntry(id),
|
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 = {
|
export const secretsManager = {
|
||||||
keys: {
|
keys: {
|
||||||
utils: {
|
utils: {
|
||||||
upsertPrivateKey,
|
upsertPrivateKey,
|
||||||
deletePrivateKey,
|
deletePrivateKey,
|
||||||
generateKeyPair,
|
|
||||||
listEntriesWithValues: betterKeyStorage.listEntriesWithValues,
|
listEntriesWithValues: betterKeyStorage.listEntriesWithValues,
|
||||||
getPrivateKey: (keyId: string) => betterKeyStorage.getEntry(keyId),
|
getPrivateKey: (keyId: string) => betterKeyStorage.getEntry(keyId),
|
||||||
// Intentionally no specialized setters; use upsertPrivateKey instead.
|
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
list: listKeysQueryOptions,
|
list: listKeysQueryOptions,
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export type ConnectionDetails = {
|
|||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
security:
|
security:
|
||||||
| { type: 'password'; password: string }
|
| { type: 'password'; password: string }
|
||||||
| { type: 'key'; privateKey: string };
|
| { type: 'key'; privateKey: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,7 +147,13 @@ export type SshShell = {
|
|||||||
type RusshApi = {
|
type RusshApi = {
|
||||||
uniffiInitAsync: () => Promise<void>;
|
uniffiInitAsync: () => Promise<void>;
|
||||||
connect: (opts: ConnectOptions) => Promise<SshConnection>;
|
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
|
// #endregion
|
||||||
@@ -312,8 +318,8 @@ function wrapConnection(
|
|||||||
term: terminalTypeLiteralToEnum[params.term],
|
term: terminalTypeLiteralToEnum[params.term],
|
||||||
onClosedCallback: params.onClosed
|
onClosedCallback: params.onClosed
|
||||||
? {
|
? {
|
||||||
onChange: (channelId) => params.onClosed!(channelId),
|
onChange: (channelId) => params.onClosed!(channelId),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
terminalMode: params.terminalMode,
|
terminalMode: params.terminalMode,
|
||||||
terminalPixelSize: params.terminalPixelSize,
|
terminalPixelSize: params.terminalPixelSize,
|
||||||
@@ -332,11 +338,11 @@ async function connect(options: ConnectOptions): Promise<SshConnection> {
|
|||||||
const security =
|
const security =
|
||||||
options.security.type === 'password'
|
options.security.type === 'password'
|
||||||
? new GeneratedRussh.Security.Password({
|
? new GeneratedRussh.Security.Password({
|
||||||
password: options.security.password,
|
password: options.security.password,
|
||||||
})
|
})
|
||||||
: new GeneratedRussh.Security.Key({
|
: new GeneratedRussh.Security.Key({
|
||||||
privateKeyContent: options.security.privateKey,
|
privateKeyContent: options.security.privateKey,
|
||||||
});
|
});
|
||||||
const sshConnection = await GeneratedRussh.connect(
|
const sshConnection = await GeneratedRussh.connect(
|
||||||
{
|
{
|
||||||
connectionDetails: {
|
connectionDetails: {
|
||||||
@@ -347,16 +353,16 @@ async function connect(options: ConnectOptions): Promise<SshConnection> {
|
|||||||
},
|
},
|
||||||
onConnectionProgressCallback: options.onConnectionProgress
|
onConnectionProgressCallback: options.onConnectionProgress
|
||||||
? {
|
? {
|
||||||
onChange: (statusEnum) =>
|
onChange: (statusEnum) =>
|
||||||
options.onConnectionProgress!(
|
options.onConnectionProgress!(
|
||||||
sshConnProgressEnumToLiteral[statusEnum]
|
sshConnProgressEnumToLiteral[statusEnum]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
onDisconnectedCallback: options.onDisconnected
|
onDisconnectedCallback: options.onDisconnected
|
||||||
? {
|
? {
|
||||||
onChange: (connectionId) => options.onDisconnected!(connectionId),
|
onChange: (connectionId) => options.onDisconnected!(connectionId),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
options.abortSignal ? { signal: options.abortSignal } : undefined
|
options.abortSignal ? { signal: options.abortSignal } : undefined
|
||||||
@@ -373,10 +379,20 @@ async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') {
|
|||||||
return GeneratedRussh.generateKeyPair(map[type]);
|
return GeneratedRussh.generateKeyPair(map[type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validatePrivateKey(key: string) {
|
||||||
|
try {
|
||||||
|
GeneratedRussh.validatePrivateKey(key);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
export const RnRussh = {
|
export const RnRussh = {
|
||||||
uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
|
uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
|
||||||
connect,
|
connect,
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
|
validatePrivateKey,
|
||||||
} satisfies RusshApi;
|
} satisfies RusshApi;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ const logExternal: boolean = false;
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react({
|
||||||
|
|
||||||
|
}),
|
||||||
dts({
|
dts({
|
||||||
tsconfigPath: './tsconfig.app.json',
|
tsconfigPath: './tsconfig.app.json',
|
||||||
// This makes dist/ look nice but breaks Cmd + Click
|
// This makes dist/ look nice but breaks Cmd + Click
|
||||||
|
|||||||
Reference in New Issue
Block a user