Secret Scan / scan (push) Successful in 8s
Build App (Preview) / compute-version (pull_request) Successful in 6s
Secret Scan / scan (pull_request) Successful in 9s
Build App (Preview) / create-release (pull_request) Successful in 5s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m59s
Build App (Preview) / build-linux (pull_request) Successful in 6m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Closes #35. Exports the host environment — global AppSettings (already the non-secret shape persisted to settings.json) plus the global secrets that live in the OS keychain instead (the shared Claude Code OAuth login, the model gateway's provider API key and master key) — to one password-encrypted file, and restores it on another machine. Per-project settings, per-project secrets, and Docker volumes are deliberately out of scope; this is not a project backup. Designed with the user in issue #35's comments: global settings only, no docker volumes, the password is the lock/key, and the export is portable as one file. Crypto (storage/settings_crypto.rs): Argon2id derives a 256-bit key from the password (memory-hard, meaningfully resistant to GPU/ASIC brute-forcing in a way PBKDF2 at any reasonable iteration count is not), AES-256-GCM does the actual encryption. A wrong password fails GCM's authentication tag rather than producing silent garbage. Salt and nonce are random per export and stored in the clear in the file header — their job is uniqueness, not secrecy. The save/open dialogs are opened from Rust, matching the boundary file_commands.rs's pick_save_path/pick_files_to_upload already establish: a frontend-driven dialog handing Rust a host path is the exact shape of bug that produced this app's past criticals. preview_settings_import resolves the chosen import path itself and remembers it (AppState::pending_settings_import) so apply_settings_import re-reads the same file without a path crossing back over IPC. The password is re-entered rather than cached between preview and apply, so nothing here holds decrypted plaintext in memory for longer than one command's execution; the preview returned to the frontend carries counts and presence flags only, never a secret value. Import replaces settings wholesale (an import is "restore this environment"), but only writes secrets actually present in the file — an absent secret means "the source machine never had this configured," not "delete this on import." Added storage::secure::store_gateway_master_key and get_gateway_master_key (read-only, unlike get_or_create_gateway_master_key which mints one as a side effect) since neither existed and import needs to restore an exact captured value rather than mint a new random one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
45 lines
1.2 KiB
TypeScript
45 lines
1.2 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useShallow } from "zustand/react/shallow";
|
|
import { useAppState } from "../store/appState";
|
|
import * as commands from "../lib/tauri-commands";
|
|
import type { AppSettings } from "../lib/types";
|
|
|
|
export function useSettings() {
|
|
const { appSettings, setAppSettings } = useAppState(
|
|
useShallow(s => ({
|
|
appSettings: s.appSettings,
|
|
setAppSettings: s.setAppSettings,
|
|
}))
|
|
);
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
try {
|
|
const settings = await commands.getSettings();
|
|
setAppSettings(settings);
|
|
return settings;
|
|
} catch (e) {
|
|
console.error("Failed to load settings:", e);
|
|
return null;
|
|
}
|
|
}, [setAppSettings]);
|
|
|
|
const saveSettings = useCallback(
|
|
async (settings: AppSettings) => {
|
|
const updated = await commands.updateSettings(settings);
|
|
setAppSettings(updated);
|
|
return updated;
|
|
},
|
|
[setAppSettings],
|
|
);
|
|
|
|
return {
|
|
appSettings,
|
|
loadSettings,
|
|
saveSettings,
|
|
/** For a command that already returns the new `AppSettings` itself
|
|
* (settings import) — updates the store without a redundant
|
|
* `updateSettings` round trip through the backend. */
|
|
setAppSettings,
|
|
};
|
|
}
|