Skip to content
Source
Sign inSign up
Open History to see the last commit

settingsSecrets.ts

294 lines10.1 KB

1import {
2 AntigravitySettings,
3 ClaudeSettings,
4 CloudSpeechCredentials,
5 CodexSettings,
6 EasDeviceHostConfig,
7 type ProviderInstanceConfig,
8 type ServerSettings,
9 type SettingsSecretValue,
10 SimlockDeviceHostConfig,
11 TextGeneratorConfig,
12 UsageLimitSourceConfig,
13 isSecretReference,
14 secretSettingsFields,
15} from "@t3tools/contracts";
16import { acpRegistryClient } from "@t3tools/provider-acp-registry/client";
17import { cursorClient } from "@t3tools/provider-cursor/client";
18import { grokClient } from "@t3tools/provider-grok/client";
19import { kiroClient } from "@t3tools/provider-kiro/client";
20import { museClient } from "@t3tools/provider-muse/client";
21import { openCodeClient } from "@t3tools/provider-opencode/client";
22import { piClient } from "@t3tools/provider-pi/client";
23import {
24 readSecretFieldValues,
25 SOURCE_CONTROL_HOST_SECRET_FIELDS,
26 sourceControlHostSecretName,
27 writeSecretFieldValues,
28} from "./sourceControl/sourceControlHostSecrets.ts";
29​
30export const SECRET_REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022";
31​
32const providerSecretFields = new Map([
33 ["codex", secretSettingsFields(CodexSettings)],
34 ["claudeAgent", secretSettingsFields(ClaudeSettings)],
35 ["antigravity", secretSettingsFields(AntigravitySettings)],
36 ...[
37 cursorClient,
38 grokClient,
39 kiroClient,
40 museClient,
41 openCodeClient,
42 piClient,
43 acpRegistryClient,
44 ].map(
45 (definition) =>
46 [definition.driverKind, secretSettingsFields(definition.settingsSchema)] as const,
47 ),
48]);
49const usageLimitSecretFields = secretSettingsFields(UsageLimitSourceConfig);
50const textGeneratorSecretFields = secretSettingsFields(TextGeneratorConfig);
51const voiceSecretFields = secretSettingsFields(CloudSpeechCredentials);
52const easDeviceHostSecretFields = secretSettingsFields(EasDeviceHostConfig);
53const simlockDeviceHostSecretFields = secretSettingsFields(SimlockDeviceHostConfig);
54const encode = (value: string) => Buffer.from(value, "utf8").toString("base64url");
55​
56export interface SettingsSecret {
57 readonly secretName: string;
58 readonly kind: "field" | "environment";
59 readonly value: string;
60 readonly redacted: boolean;
61 readonly sensitive: boolean;
62 readonly providerInstanceId?: string;
63 readonly environmentVariable?: string;
65​
66type SecretValue = Pick<SettingsSecret, "value" | "redacted">;
67type SecretSlotName = (field: string, serverHost: string | null) => string;
68type SecretFieldsMapper = (
69 fields: Readonly<Record<string, unknown>>,
70 secretFields: ReadonlyArray<string>,
71 name: SecretSlotName,
72) => Record<string, unknown>;
73type ProviderEnvironment = NonNullable<ProviderInstanceConfig["environment"]>;
74​
75export function mapSettingsSecrets(
76 settings: ServerSettings,
77 map: (secret: SettingsSecret) => SecretValue,
78): ServerSettings {
79 const mapFields: SecretFieldsMapper = (fields, secretFields, name) => {
80 const next = { ...fields };
81 for (const field of secretFields) {
82 if (!(field in next) || isSecretReference(next[field])) continue;
83 next[field] = writeSecretFieldValues(
84 next[field],
85 readSecretFieldValues(next[field]).map(([serverHost, value]) => {
86 const result = map({
87 secretName: name(field, serverHost),
88 kind: "field",
89 value,
90 redacted: value === SECRET_REDACTED,
91 sensitive: true,
92 });
93 return [serverHost, result.redacted ? SECRET_REDACTED : result.value] as const;
94 }),
95 );
96 }
97 return next;
98 };
99​
100 return mapSecretFieldGroups(settings, mapFields, (instanceId, environment) =>
101 environment.map((variable) => {
102 const { valueRedacted: _omit, ...rest } = variable;
103 // A secret reference is not itself secret; storage sees the value cleared.
104 const reference = typeof variable.value === "string" ? undefined : variable.value;
105 const result = map({
106 secretName: `provider-env-${encode(instanceId)}-${encode(variable.name)}`,
107 kind: "environment",
108 value: typeof variable.value === "string" ? variable.value : "",
109 redacted: reference === undefined && variable.valueRedacted === true,
110 sensitive: variable.sensitive,
111 providerInstanceId: instanceId,
112 environmentVariable: variable.name,
113 });
114 if (reference !== undefined) return { ...rest, value: reference };
115 return {
116 ...rest,
117 value: result.value,
118 ...(result.redacted ? { valueRedacted: true } : {}),
119 };
120 }),
121 );
123​
124/**
125 * Maps each value a secret field holds, literal or reference, with the secret name of its slot.
126 * Provider environment variables are left as they are.
127 */
128export function mapSettingsSecretValues(
129 settings: ServerSettings,
130 map: (value: SettingsSecretValue, secretName: string) => SettingsSecretValue,
131): ServerSettings {
132 const mapValue = (value: unknown, secretName: string) =>
133 typeof value === "string" || isSecretReference(value) ? map(value, secretName) : value;
134 return mapSecretFieldGroups(settings, (fields, secretFields, name) => {
135 const next = { ...fields };
136 for (const field of secretFields) {
137 if (!(field in next)) continue;
138 const value = next[field];
139 next[field] =
140 value !== null &&
141 typeof value === "object" &&
142 !Array.isArray(value) &&
143 !isSecretReference(value)
144 ? Object.fromEntries(
145 Object.entries(value).map(([serverHost, entry]) => [
146 serverHost,
147 mapValue(entry, name(field, serverHost.trim().toLowerCase())),
148 ]),
149 )
150 : mapValue(value, name(field, null));
151 }
152 return next;
153 });
155​
156function mapSecretFieldGroups(
157 settings: ServerSettings,
158 mapFields: SecretFieldsMapper,
159 mapEnvironment: (instanceId: string, environment: ProviderEnvironment) => ProviderEnvironment = (
160 _instanceId,
161 environment,
162 ) => environment,
163): ServerSettings {
164 const providerInstances = Object.fromEntries(
165 Object.entries(settings.providerInstances).map(([instanceId, instance]) => {
166 const config = instance.config;
167 return [
168 instanceId,
169 {
170 ...instance,
171 ...(config !== null && typeof config === "object" && !Array.isArray(config)
172 ? {
173 config: mapFields(
174 config as Record<string, unknown>,
175 providerSecretFields.get(instance.driver) ?? [],
176 (field, serverHost) =>
177 `provider-config-${encode(instanceId)}-${encode(instance.driver)}-${encode(field)}` +
178 (serverHost === null ? "" : `-${encode(serverHost)}`),
179 ),
180 }
181 : {}),
182 ...(instance.environment
183 ? { environment: mapEnvironment(instanceId, instance.environment) }
184 : {}),
185 },
186 ];
187 }),
188 );
189​
190 const sourceControlHosts = Object.fromEntries(
191 Object.entries(settings.sourceControlHosts).map(([kind, fields]) => [
192 kind,
193 mapFields(fields, SOURCE_CONTROL_HOST_SECRET_FIELDS.get(kind) ?? [], (field, serverHost) =>
194 sourceControlHostSecretName({ kind, field, serverHost }),
195 ),
196 ]),
197 ) as ServerSettings["sourceControlHosts"];
198​
199 const usageLimitSources = Object.fromEntries(
200 Object.entries(settings.usageLimitSources).map(([id, source]) => [
201 id,
202 mapFields(
203 source,
204 usageLimitSecretFields,
205 (field, serverHost) =>
206 `usage-limit-source-${encode(id)}` +
207 (field === "managementKey" && serverHost === null
208 ? ""
209 : `-${encode(field)}${serverHost === null ? "" : `-${encode(serverHost)}`}`),
210 ),
211 ]),
212 ) as ServerSettings["usageLimitSources"];
213​
214 const textGenerators = Object.fromEntries(
215 Object.entries(settings.textGenerators).map(([id, generator]) => [
216 id,
217 mapFields(
218 generator,
219 textGeneratorSecretFields,
220 (field, serverHost) =>
221 `text-generator-${encode(id)}-${encode(field)}` +
222 (serverHost === null ? "" : `-${encode(serverHost)}`),
223 ),
224 ]),
225 ) as ServerSettings["textGenerators"];
226​
227 const easDeviceHosts = settings.easDeviceHosts.map(
228 (host) =>
229 mapFields(
230 host,
231 easDeviceHostSecretFields,
232 (field, serverHost) =>
233 `device-host-${encode(host.id)}-${encode(field)}` +
234 (serverHost === null ? "" : `-${encode(serverHost)}`),
235 ) as EasDeviceHostConfig,
236 );
237​
238 // Its own prefix: an EAS and a Simlock host may share an id.
239 const simlockDeviceHosts = settings.simlockDeviceHosts.map(
240 (host) =>
241 mapFields(
242 host,
243 simlockDeviceHostSecretFields,
244 (field, serverHost) =>
245 `simlock-device-host-${encode(host.id)}-${encode(field)}` +
246 (serverHost === null ? "" : `-${encode(serverHost)}`),
247 ) as SimlockDeviceHostConfig,
248 );
249​
250 return {
251 ...settings,
252 cloudSpeechCredentials: mapFields(
253 settings.cloudSpeechCredentials,
254 voiceSecretFields,
255 (provider) => `voice-cloud-${provider}`,
256 ) as ServerSettings["cloudSpeechCredentials"],
257 providerInstances,
258 sourceControlHosts,
259 usageLimitSources,
260 textGenerators,
261 easDeviceHosts,
262 simlockDeviceHosts,
263 };
265​
266/**
267 * `next` with every secret reference to a secret provider that `current` has and `next` dropped
268 * unset: a credential field becomes empty, and a provider variable holding one is removed.
269 */
270export function withoutRemovedSecretProviderReferences(
271 current: ServerSettings,
272 next: ServerSettings,
273): ServerSettings {
274 const removed = new Set(
275 Object.keys(current.secretProviders).filter((id) => !Object.hasOwn(next.secretProviders, id)),
276 );
277 if (removed.size === 0) return next;
278 const isRemoved = (value: unknown) => isSecretReference(value) && removed.has(value.provider);
279 const fields = mapSettingsSecretValues(next, (value) => (isRemoved(value) ? "" : value));
280 return {
281 ...fields,
282 providerInstances: Object.fromEntries(
283 Object.entries(fields.providerInstances).map(([instanceId, instance]) => [
284 instanceId,
285 instance.environment?.some((variable) => isRemoved(variable.value))
286 ? {
287 ...instance,
288 environment: instance.environment.filter((variable) => !isRemoved(variable.value)),
289 }
290 : instance,
291 ]),
292 ),
293 };