File size: 4,900 Bytes
63522a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { SettingsClient } from "@openhands/typescript-client/clients";
import { isSdkHttpStatusError } from "./agent-server-compatibility";
import { getActiveBackend } from "./backend-registry/active-store";
import {
  deleteCloudSecret,
  fetchCloudSecrets,
  saveCloudSecret,
} from "./cloud/secrets-service.api";
import { getAgentServerClientOptions } from "./agent-server-client-options";
import { CustomSecretWithoutValue } from "./secrets-service.types";
import { withRetry } from "./with-retry";

async function fetchSecrets(): Promise<CustomSecretWithoutValue[]> {
  if (getActiveBackend().backend.kind === "cloud") {
    return withRetry(() => fetchCloudSecrets());
  }
  const response = await withRetry(() =>
    new SettingsClient(getAgentServerClientOptions()).listSecrets(),
  );
  return response.secrets.map((s) => ({
    name: s.name,
    description: s.description,
  }));
}

export class SecretsService {
  /**
   * List all custom secrets (names and descriptions only, no values).
   * Uses the agent-server API endpoint: GET /api/settings/secrets
   *
   * Note: The agent-server API doesn't support pagination or search filtering.
   * All secrets are returned in a single response.
   */
  static async getSecrets(): Promise<CustomSecretWithoutValue[]> {
    try {
      return await fetchSecrets();
    } catch (error) {
      console.error("Failed to fetch secrets after retries:", error);
      return [];
    }
  }

  /**
   * List all custom secrets, surfacing failures to callers that must
   * distinguish an unavailable list from an empty one.
   */
  static async getSecretsOrThrow(): Promise<CustomSecretWithoutValue[]> {
    return fetchSecrets();
  }

  /**
   * Create or update a custom secret (upsert by name).
   * Uses the agent-server API endpoint: PUT /api/settings/secrets
   *
   * @param name - Secret name (must start with letter, contain only letters/numbers/underscores, 1-64 chars)
   * @param value - Secret value
   * @param description - Optional description
   * @throws Error if the API call fails after retries
   */
  static async createSecret(
    name: string,
    value: string,
    description?: string,
  ): Promise<void> {
    if (getActiveBackend().backend.kind === "cloud") {
      await saveCloudSecret({ name, value, description });
      return;
    }
    await withRetry(() =>
      new SettingsClient(getAgentServerClientOptions()).upsertSecret({
        name,
        value,
        description,
      }),
    );
  }

  /**
   * Update a secret's name and/or description, and optionally overwrite its
   * value. When no value is given the existing one is preserved: the
   * agent-server only exposes an upsert endpoint, so we fetch the existing
   * value and re-upsert it under the updated name/description.
   *
   * @param secretToEdit - Existing secret name
   * @param name - New (or same) secret name
   * @param description - Optional new description
   * @param value - Optional new value; when omitted the stored value is kept
   * @throws Error if the API call fails after retries
   */
  static async updateSecret(
    secretToEdit: string,
    name: string,
    description?: string,
    value?: string,
  ): Promise<void> {
    if (getActiveBackend().backend.kind === "cloud") {
      await saveCloudSecret({
        name,
        value,
        description,
        previousName: secretToEdit,
      });
      return;
    }

    const client = new SettingsClient(getAgentServerClientOptions());
    const nextValue =
      value ?? (await withRetry(() => client.getSecret(secretToEdit)));

    await withRetry(() =>
      client.upsertSecret({
        name,
        value: nextValue,
        description,
      }),
    );

    if (name !== secretToEdit) {
      await this.deleteSecret(secretToEdit);
    }
  }

  /**
   * Delete a custom secret by name.
   * Uses the agent-server API endpoint: DELETE /api/settings/secrets/{name}
   *
   * @param name - Secret name to delete
   * @throws Error if the API call fails (except 404, which is treated as success)
   */
  static async deleteSecret(name: string): Promise<void> {
    try {
      if (getActiveBackend().backend.kind === "cloud") {
        await withRetry(() => deleteCloudSecret(name));
        return;
      }
      await withRetry(() =>
        new SettingsClient(getAgentServerClientOptions()).deleteSecret(name),
      );
    } catch (error) {
      // 404 means secret doesn't exist - treat as successful deletion.
      // Both the SDK's HttpError (status on the error itself) and
      // axios-style errors (status under `response`) count.
      if (
        isSdkHttpStatusError(error, 404) ||
        (error &&
          typeof error === "object" &&
          "response" in error &&
          (error as { response?: { status?: number } }).response?.status ===
            404)
      ) {
        return;
      }
      throw error;
    }
  }
}