diff --git a/cmd/wsh/cmd/wshcmd-secret.go b/cmd/wsh/cmd/wshcmd-secret.go new file mode 100644 index 0000000000..14b0ea2781 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-secret.go @@ -0,0 +1,130 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + "regexp" + "strings" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +// secretNameRegex must match the validation in pkg/wconfig/secretstore.go +var secretNameRegex = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) + +var secretCmd = &cobra.Command{ + Use: "secret", + Short: "manage secrets", + Long: "Manage secrets for Wave Terminal", +} + +var secretGetCmd = &cobra.Command{ + Use: "get [name]", + Short: "get a secret value", + Args: cobra.ExactArgs(1), + RunE: secretGetRun, + PreRunE: preRunSetupRpcClient, +} + +var secretSetCmd = &cobra.Command{ + Use: "set [name]=[value]", + Short: "set a secret value", + Args: cobra.ExactArgs(1), + RunE: secretSetRun, + PreRunE: preRunSetupRpcClient, +} + +var secretListCmd = &cobra.Command{ + Use: "list", + Short: "list all secret names", + Args: cobra.NoArgs, + RunE: secretListRun, + PreRunE: preRunSetupRpcClient, +} + +func init() { + rootCmd.AddCommand(secretCmd) + secretCmd.AddCommand(secretGetCmd) + secretCmd.AddCommand(secretSetCmd) + secretCmd.AddCommand(secretListCmd) +} + +func secretGetRun(cmd *cobra.Command, args []string) (rtnErr error) { + defer func() { + sendActivity("secret", rtnErr == nil) + }() + + name := args[0] + if !secretNameRegex.MatchString(name) { + return fmt.Errorf("invalid secret name: must start with a letter and contain only letters, numbers, and underscores") + } + + resp, err := wshclient.GetSecretsCommand(RpcClient, []string{name}, &wshrpc.RpcOpts{Timeout: 2000}) + if err != nil { + return fmt.Errorf("getting secret: %w", err) + } + + value, ok := resp[name] + if !ok { + return fmt.Errorf("secret not found: %s", name) + } + + WriteStdout("%s\n", value) + return nil +} + +func secretSetRun(cmd *cobra.Command, args []string) (rtnErr error) { + defer func() { + sendActivity("secret", rtnErr == nil) + }() + + parts := strings.SplitN(args[0], "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid format: expected [name]=[value]") + } + + name := parts[0] + value := parts[1] + + if name == "" { + return fmt.Errorf("secret name cannot be empty") + } + + backend, err := wshclient.GetSecretsLinuxStorageBackendCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000}) + if err != nil { + return fmt.Errorf("checking secret storage backend: %w", err) + } + + if backend == "basic_text" || backend == "unknown" { + return fmt.Errorf("No appropriate secret manager found, cannot set secrets") + } + + secrets := map[string]string{name: value} + err = wshclient.SetSecretsCommand(RpcClient, secrets, &wshrpc.RpcOpts{Timeout: 2000}) + if err != nil { + return fmt.Errorf("setting secret: %w", err) + } + + WriteStdout("secret set: %s\n", name) + return nil +} + +func secretListRun(cmd *cobra.Command, args []string) (rtnErr error) { + defer func() { + sendActivity("secret", rtnErr == nil) + }() + + names, err := wshclient.GetSecretsNamesCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000}) + if err != nil { + return fmt.Errorf("listing secrets: %w", err) + } + + for _, name := range names { + WriteStdout("%s\n", name) + } + return nil +} \ No newline at end of file diff --git a/emain/emain-wsh.ts b/emain/emain-wsh.ts index aaf330038a..639f4dfd35 100644 --- a/emain/emain-wsh.ts +++ b/emain/emain-wsh.ts @@ -4,7 +4,7 @@ import { WindowService } from "@/app/store/services"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; -import { Notification } from "electron"; +import { Notification, safeStorage } from "electron"; import { getResolvedUpdateChannel } from "emain/updater"; import { unamePlatform } from "./emain-platform"; import { getWebContentsByBlockId, webGetSelector } from "./emain-web"; @@ -60,6 +60,48 @@ export class ElectronWshClientType extends WshClient { ww.focus(); } + async handle_electronencrypt( + rh: RpcResponseHelper, + data: CommandElectronEncryptData + ): Promise { + if (!safeStorage.isEncryptionAvailable()) { + throw new Error("encryption is not available"); + } + const encrypted = safeStorage.encryptString(data.plaintext); + const ciphertext = encrypted.toString("base64"); + + let storagebackend = ""; + if (process.platform === "linux") { + storagebackend = safeStorage.getSelectedStorageBackend(); + } + + return { + ciphertext, + storagebackend, + }; + } + + async handle_electrondecrypt( + rh: RpcResponseHelper, + data: CommandElectronDecryptData + ): Promise { + if (!safeStorage.isEncryptionAvailable()) { + throw new Error("encryption is not available"); + } + const encrypted = Buffer.from(data.ciphertext, "base64"); + const plaintext = safeStorage.decryptString(encrypted); + + let storagebackend = ""; + if (process.platform === "linux") { + storagebackend = safeStorage.getSelectedStorageBackend(); + } + + return { + plaintext, + storagebackend, + }; + } + // async handle_workspaceupdate(rh: RpcResponseHelper) { // console.log("workspaceupdate"); // fireAndForget(async () => { diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index e1e71f3bca..5b297ad441 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -147,6 +147,16 @@ class RpcApiType { return client.wshRpcCall("disposesuggestions", data, opts); } + // command "electrondecrypt" [call] + ElectronDecryptCommand(client: WshClient, data: CommandElectronDecryptData, opts?: RpcOpts): Promise { + return client.wshRpcCall("electrondecrypt", data, opts); + } + + // command "electronencrypt" [call] + ElectronEncryptCommand(client: WshClient, data: CommandElectronEncryptData, opts?: RpcOpts): Promise { + return client.wshRpcCall("electronencrypt", data, opts); + } + // command "eventpublish" [call] EventPublishCommand(client: WshClient, data: WaveEvent, opts?: RpcOpts): Promise { return client.wshRpcCall("eventpublish", data, opts); @@ -297,6 +307,21 @@ class RpcApiType { return client.wshRpcCall("getrtinfo", data, opts); } + // command "getsecrets" [call] + GetSecretsCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise<{[key: string]: string}> { + return client.wshRpcCall("getsecrets", data, opts); + } + + // command "getsecretslinuxstoragebackend" [call] + GetSecretsLinuxStorageBackendCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("getsecretslinuxstoragebackend", null, opts); + } + + // command "getsecretsnames" [call] + GetSecretsNamesCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("getsecretsnames", null, opts); + } + // command "gettab" [call] GetTabCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { return client.wshRpcCall("gettab", data, opts); @@ -472,6 +497,11 @@ class RpcApiType { return client.wshRpcCall("setrtinfo", data, opts); } + // command "setsecrets" [call] + SetSecretsCommand(client: WshClient, data: {[key: string]: string}, opts?: RpcOpts): Promise { + return client.wshRpcCall("setsecrets", data, opts); + } + // command "setvar" [call] SetVarCommand(client: WshClient, data: CommandVarData, opts?: RpcOpts): Promise { return client.wshRpcCall("setvar", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 1de284dee5..11dd2bfed3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -227,6 +227,28 @@ declare global { routeid: string; }; + // wshrpc.CommandElectronDecryptData + type CommandElectronDecryptData = { + ciphertext: string; + }; + + // wshrpc.CommandElectronDecryptRtnData + type CommandElectronDecryptRtnData = { + plaintext: string; + storagebackend: string; + }; + + // wshrpc.CommandElectronEncryptData + type CommandElectronEncryptData = { + plaintext: string; + }; + + // wshrpc.CommandElectronEncryptRtnData + type CommandElectronEncryptRtnData = { + ciphertext: string; + storagebackend: string; + }; + // wshrpc.CommandEventReadHistoryData type CommandEventReadHistoryData = { event: string; diff --git a/package-lock.json b/package-lock.json index 6f653bc1c4..f5604304b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.2-beta.4", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.2-beta.4", + "version": "0.12.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/secretstore/secretstore.go b/pkg/secretstore/secretstore.go new file mode 100644 index 0000000000..0621e34672 --- /dev/null +++ b/pkg/secretstore/secretstore.go @@ -0,0 +1,288 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package secretstore + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "sync" + "time" + + "github.com/wavetermdev/waveterm/pkg/wavebase" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" + "github.com/wavetermdev/waveterm/pkg/wshutil" +) + +const ( + SecretsFileName = "secrets.enc" + WriteDebounceMs = 1000 + EncryptionTimeout = 5000 + InitRetryMs = 1000 + SecretNamePattern = `^[A-Za-z][A-Za-z0-9_]*$` + WriteTsKey = "wave:writets" +) + +var lock sync.Mutex +var secrets = make(map[string]string) +var writeRequestChan chan struct{} +var initialized bool +var lastInitTryTime time.Time +var lastInitErr error +var secretNameRegexp = regexp.MustCompile(SecretNamePattern) +var linuxStorageBackend string + +// must hold lock +func getLinuxStorageBackend() error { + if runtime.GOOS != "linux" { + return nil + } + + rpcClient := wshclient.GetBareRpcClient() + ctx, cancel := context.WithTimeout(context.Background(), EncryptionTimeout*time.Millisecond) + defer cancel() + + encryptData := wshrpc.CommandElectronEncryptData{ + PlainText: "hello", + } + rpcOpts := &wshrpc.RpcOpts{ + Route: wshutil.ElectronRoute, + Timeout: EncryptionTimeout, + } + + result, err := wshclient.ElectronEncryptCommand(rpcClient, encryptData, rpcOpts) + if err != nil { + return fmt.Errorf("failed to get storage backend: %w", err) + } + + if ctx.Err() != nil { + return fmt.Errorf("encryption timeout: %w", ctx.Err()) + } + + if result.StorageBackend != "" { + linuxStorageBackend = result.StorageBackend + } + + return nil +} + +// must hold lock +func readSecretsFromFile() (map[string]string, error) { + configDir := wavebase.GetWaveConfigDir() + secretsPath := filepath.Join(configDir, SecretsFileName) + + encryptedData, err := os.ReadFile(secretsPath) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("secretstore: could not read secrets file: %v\n", err) + } + if err := getLinuxStorageBackend(); err != nil { + log.Printf("secretstore: could not get linux storage backend: %v\n", err) + } + return make(map[string]string), nil + } + + rpcClient := wshclient.GetBareRpcClient() + ctx, cancel := context.WithTimeout(context.Background(), EncryptionTimeout*time.Millisecond) + defer cancel() + + decryptData := wshrpc.CommandElectronDecryptData{ + CipherText: string(encryptedData), + } + rpcOpts := &wshrpc.RpcOpts{ + Route: wshutil.ElectronRoute, + Timeout: EncryptionTimeout, + } + + result, err := wshclient.ElectronDecryptCommand(rpcClient, decryptData, rpcOpts) + if err != nil { + return nil, fmt.Errorf("failed to decrypt secrets: %w", err) + } + + if ctx.Err() != nil { + return nil, fmt.Errorf("decryption timeout: %w", ctx.Err()) + } + + if result.StorageBackend != "" { + linuxStorageBackend = result.StorageBackend + } + + var decryptedSecrets map[string]string + if err := json.Unmarshal([]byte(result.PlainText), &decryptedSecrets); err != nil { + return nil, fmt.Errorf("failed to parse secrets: %w", err) + } + + return decryptedSecrets, nil +} + +func initSecretStore() error { + lock.Lock() + defer lock.Unlock() + if initialized { + return nil + } + + now := time.Now() + if !lastInitTryTime.IsZero() && now.Sub(lastInitTryTime) < InitRetryMs*time.Millisecond { + return lastInitErr + } + + lastInitTryTime = now + loadedSecrets, err := readSecretsFromFile() + if err != nil { + lastInitErr = err + return err + } + secrets = loadedSecrets + + writeRequestChan = make(chan struct{}, 1) + initialized = true + lastInitErr = nil + go writerLoop() + return nil +} + +func writerLoop() { + var timer *time.Timer + for range writeRequestChan { + if timer != nil { + timer.Stop() + } + timer = time.AfterFunc(WriteDebounceMs*time.Millisecond, func() { + if err := writeSecretsToFile(); err != nil { + log.Printf("secretstore: error writing secrets: %v\n", err) + } + }) + } +} + +func writeSecretsToFile() error { + lock.Lock() + secretsCopy := make(map[string]string, len(secrets)+1) + for k, v := range secrets { + secretsCopy[k] = v + } + secretsCopy[WriteTsKey] = time.Now().UTC().Format(time.RFC3339) + lock.Unlock() + + jsonData, err := json.Marshal(secretsCopy) + if err != nil { + return fmt.Errorf("failed to marshal secrets: %w", err) + } + + rpcClient := wshclient.GetBareRpcClient() + ctx, cancel := context.WithTimeout(context.Background(), EncryptionTimeout*time.Millisecond) + defer cancel() + + encryptData := wshrpc.CommandElectronEncryptData{ + PlainText: string(jsonData), + } + rpcOpts := &wshrpc.RpcOpts{ + Route: wshutil.ElectronRoute, + Timeout: EncryptionTimeout, + } + + result, err := wshclient.ElectronEncryptCommand(rpcClient, encryptData, rpcOpts) + if err != nil { + return fmt.Errorf("failed to encrypt secrets: %w", err) + } + + if ctx.Err() != nil { + return fmt.Errorf("encryption timeout: %w", ctx.Err()) + } + + configDir := wavebase.GetWaveConfigDir() + secretsPath := filepath.Join(configDir, SecretsFileName) + + if err := os.WriteFile(secretsPath, []byte(result.CipherText), 0600); err != nil { + return fmt.Errorf("failed to write secrets file: %w", err) + } + + return nil +} + +func requestWrite() { + select { + case writeRequestChan <- struct{}{}: + default: + } +} + +func SetSecret(name string, value string) error { + if name == "" { + return fmt.Errorf("secret name cannot be empty") + } + if !secretNameRegexp.MatchString(name) { + return fmt.Errorf("secret name must start with a letter and contain only letters, numbers, and underscores") + } + if err := initSecretStore(); err != nil { + return err + } + lock.Lock() + defer lock.Unlock() + + secrets[name] = value + requestWrite() + return nil +} + +func GetSecret(name string) (string, bool, error) { + if name == WriteTsKey { + return "", false, nil + } + if err := initSecretStore(); err != nil { + return "", false, err + } + lock.Lock() + defer lock.Unlock() + + value, exists := secrets[name] + return value, exists, nil +} + +func GetSecretNames() ([]string, error) { + if err := initSecretStore(); err != nil { + return nil, err + } + lock.Lock() + defer lock.Unlock() + + names := make([]string, 0, len(secrets)) + for name := range secrets { + if name == WriteTsKey { + continue + } + names = append(names, name) + } + return names, nil +} + +func GetLinuxStorageBackend() (string, error) { + if runtime.GOOS != "linux" { + return "", nil + } + + lock.Lock() + defer lock.Unlock() + + if linuxStorageBackend != "" { + return linuxStorageBackend, nil + } + + if err := getLinuxStorageBackend(); err != nil { + return "", err + } + + if linuxStorageBackend == "" { + return "", fmt.Errorf("failed to determine linux storage backend") + } + + return linuxStorageBackend, nil +} diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 2a6210358c..2505f2c192 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -185,6 +185,18 @@ func DisposeSuggestionsCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcO return err } +// command "electrondecrypt", wshserver.ElectronDecryptCommand +func ElectronDecryptCommand(w *wshutil.WshRpc, data wshrpc.CommandElectronDecryptData, opts *wshrpc.RpcOpts) (*wshrpc.CommandElectronDecryptRtnData, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.CommandElectronDecryptRtnData](w, "electrondecrypt", data, opts) + return resp, err +} + +// command "electronencrypt", wshserver.ElectronEncryptCommand +func ElectronEncryptCommand(w *wshutil.WshRpc, data wshrpc.CommandElectronEncryptData, opts *wshrpc.RpcOpts) (*wshrpc.CommandElectronEncryptRtnData, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.CommandElectronEncryptRtnData](w, "electronencrypt", data, opts) + return resp, err +} + // command "eventpublish", wshserver.EventPublishCommand func EventPublishCommand(w *wshutil.WshRpc, data wps.WaveEvent, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "eventpublish", data, opts) @@ -362,6 +374,24 @@ func GetRTInfoCommand(w *wshutil.WshRpc, data wshrpc.CommandGetRTInfoData, opts return resp, err } +// command "getsecrets", wshserver.GetSecretsCommand +func GetSecretsCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpts) (map[string]string, error) { + resp, err := sendRpcRequestCallHelper[map[string]string](w, "getsecrets", data, opts) + return resp, err +} + +// command "getsecretslinuxstoragebackend", wshserver.GetSecretsLinuxStorageBackendCommand +func GetSecretsLinuxStorageBackendCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (string, error) { + resp, err := sendRpcRequestCallHelper[string](w, "getsecretslinuxstoragebackend", nil, opts) + return resp, err +} + +// command "getsecretsnames", wshserver.GetSecretsNamesCommand +func GetSecretsNamesCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]string, error) { + resp, err := sendRpcRequestCallHelper[[]string](w, "getsecretsnames", nil, opts) + return resp, err +} + // command "gettab", wshserver.GetTabCommand func GetTabCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*waveobj.Tab, error) { resp, err := sendRpcRequestCallHelper[*waveobj.Tab](w, "gettab", data, opts) @@ -568,6 +598,12 @@ func SetRTInfoCommand(w *wshutil.WshRpc, data wshrpc.CommandSetRTInfoData, opts return err } +// command "setsecrets", wshserver.SetSecretsCommand +func SetSecretsCommand(w *wshutil.WshRpc, data map[string]string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "setsecrets", data, opts) + return err +} + // command "setvar", wshserver.SetVarCommand func SetVarCommand(w *wshutil.WshRpc, data wshrpc.CommandVarData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "setvar", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index a53e0aa441..c5d96088ec 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -165,6 +165,16 @@ const ( Command_StartBuilder = "startbuilder" Command_GetBuilderStatus = "getbuilderstatus" Command_GetBuilderOutput = "getbuilderoutput" + + // electron + Command_ElectronEncrypt = "electronencrypt" + Command_ElectronDecrypt = "electrondecrypt" + + // secrets + Command_GetSecrets = "getsecrets" + Command_GetSecretsNames = "getsecretsnames" + Command_SetSecrets = "setsecrets" + Command_GetSecretsLinuxStorageBackend = "getsecretslinuxstoragebackend" ) type RespOrErrorUnion[T any] struct { @@ -275,6 +285,14 @@ type WshRpcInterface interface { WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error) NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error FocusWindowCommand(ctx context.Context, windowId string) error + ElectronEncryptCommand(ctx context.Context, data CommandElectronEncryptData) (*CommandElectronEncryptRtnData, error) + ElectronDecryptCommand(ctx context.Context, data CommandElectronDecryptData) (*CommandElectronDecryptRtnData, error) + + // secrets + GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) + GetSecretsNamesCommand(ctx context.Context) ([]string, error) + SetSecretsCommand(ctx context.Context, secrets map[string]string) error + GetSecretsLinuxStorageBackendCommand(ctx context.Context) (string, error) WorkspaceListCommand(ctx context.Context) ([]WorkspaceInfoData, error) GetUpdateChannelCommand(ctx context.Context) (string, error) @@ -600,8 +618,8 @@ type CommandFileCopyData struct { } type CommandFileRestoreBackupData struct { - BackupFilePath string `json:"backupfilepath"` - RestoreToFileName string `json:"restoretofilename"` + BackupFilePath string `json:"backupfilepath"` + RestoreToFileName string `json:"restoretofilename"` } type CommandRemoteStreamTarData struct { @@ -986,3 +1004,21 @@ type BuilderStatusData struct { ErrorMsg string `json:"errormsg,omitempty"` Version int `json:"version"` } + +type CommandElectronEncryptData struct { + PlainText string `json:"plaintext"` +} + +type CommandElectronEncryptRtnData struct { + CipherText string `json:"ciphertext"` + StorageBackend string `json:"storagebackend"` // only returned for linux +} + +type CommandElectronDecryptData struct { + CipherText string `json:"ciphertext"` +} + +type CommandElectronDecryptRtnData struct { + PlainText string `json:"plaintext"` + StorageBackend string `json:"storagebackend"` // only returned for linux +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 73639c42a7..df7f7a29c1 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -34,6 +34,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/remote/awsconn" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" "github.com/wavetermdev/waveterm/pkg/remote/fileshare" + "github.com/wavetermdev/waveterm/pkg/secretstore" "github.com/wavetermdev/waveterm/pkg/suggestion" "github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata" @@ -1240,3 +1241,43 @@ func (ws *WshServer) GetTabCommand(ctx context.Context, tabId string) (*waveobj. } return tab, nil } + +func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) { + result := make(map[string]string) + for _, name := range names { + value, exists, err := secretstore.GetSecret(name) + if err != nil { + return nil, fmt.Errorf("error getting secret %q: %w", name, err) + } + if exists { + result[name] = value + } + } + return result, nil +} + +func (ws *WshServer) GetSecretsNamesCommand(ctx context.Context) ([]string, error) { + names, err := secretstore.GetSecretNames() + if err != nil { + return nil, fmt.Errorf("error getting secret names: %w", err) + } + return names, nil +} + +func (ws *WshServer) SetSecretsCommand(ctx context.Context, secrets map[string]string) error { + for name, value := range secrets { + err := secretstore.SetSecret(name, value) + if err != nil { + return fmt.Errorf("error setting secret %q: %w", name, err) + } + } + return nil +} + +func (ws *WshServer) GetSecretsLinuxStorageBackendCommand(ctx context.Context) (string, error) { + backend, err := secretstore.GetLinuxStorageBackend() + if err != nil { + return "", fmt.Errorf("error getting linux storage backend: %w", err) + } + return backend, nil +}