Skip to content
Source
Sign inSign up

sourceControlHostSecrets.ts

144 lines5.4 KB

1import {
2 isSecretReference,
3 secretReferencesInSettings,
4 secretSettingsFields,
5 type SecretReference,
6 type ServerSettings,
7} from "@t3tools/contracts";
8import type * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver";
9import * as Effect from "effect/Effect";
10​
11import { resolveSettingsSecrets } from "../secrets/settingsSecretValue.ts";
12​
13import { sourceControlDefinitions } from "./sourceControlDefinitions.ts";
14​
15export const SOURCE_CONTROL_HOST_SECRET_FIELDS: ReadonlyMap<
16 string,
17 ReadonlyArray<string>
18> = new Map(
19 sourceControlDefinitions.definitions.flatMap((definition) => {
20 const fields = definition.settings ? secretSettingsFields(definition.settings) : [];
21 return fields.length > 0 ? [[definition.kind, fields] as const] : [];
22 }),
23);
24​
25export interface SourceControlHostSecretSlot {
26 readonly kind: string;
27 readonly field: string;
28 readonly serverHost: string | null;
30​
31/** The secret store entry for one slot; names stay stable across releases. */
32export function sourceControlHostSecretName(slot: SourceControlHostSecretSlot): string {
33 const encode = (value: string) => Buffer.from(value, "utf8").toString("base64url");
34 return slot.serverHost === null
35 ? `source-control-${encode(slot.kind)}-${encode(slot.field)}`
36 : `source-control-${encode(slot.kind)}-${encode(slot.field)}-${encode(slot.serverHost.trim().toLowerCase())}`;
38​
39export function readSecretFieldValues(
40 value: unknown,
41): ReadonlyArray<readonly [serverHost: string | null, value: string]> {
42 if (typeof value === "string") return [[null, value]];
43 if (isSecretReference(value)) return [];
44 if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
45 return Object.entries(value).flatMap(([serverHost, entry]) =>
46 typeof entry === "string" ? [[serverHost.trim().toLowerCase(), entry] as const] : [],
47 );
49​
50export function writeSecretFieldValues(
51 original: unknown,
52 values: ReadonlyArray<readonly [serverHost: string | null, value: string]>,
53): unknown {
54 if (typeof original === "string") return values[0]?.[1] ?? "";
55 if (isSecretReference(original)) return original;
56 const sources =
57 original !== null && typeof original === "object" && !Array.isArray(original)
58 ? Object.entries(original).flatMap(([serverHost, entry]) =>
59 isSecretReference(entry) ? [[serverHost.trim().toLowerCase(), entry] as const] : [],
60 )
61 : [];
62 return Object.fromEntries([
63 ...sources,
64 ...values.map(([serverHost, value]) => [serverHost ?? "", value] as const),
65 ]);
67​
68type SourceControlHostsSettings = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
69​
70const hostSettingsSchemas = new Map(
71 sourceControlDefinitions.definitions.flatMap((definition) =>
72 definition.settings ? [[definition.kind as string, definition.settings] as const] : [],
73 ),
74);
75​
76/** Every secret provider reference held in any host kind's credential fields. */
77export function collectSourceControlHostSecretReferences(
78 hosts: SourceControlHostsSettings,
79): ReadonlyArray<SecretReference> {
80 return [...hostSettingsSchemas].flatMap(([kind, schema]) =>
81 secretReferencesInSettings(schema, hosts[kind]).map(({ reference }) => reference),
82 );
84​
85/** Replaces each secret provider reference in one host kind's credential fields with the secret it names. */
86export const resolveSourceControlHostSecrets = Effect.fn("SourceControlHost.resolveSecrets")(
87 function* (
88 resolver: SecretResolver.SecretResolver["Service"],
89 hosts: SourceControlHostsSettings,
90 kind: string,
91 ) {
92 const schema = hostSettingsSchemas.get(kind);
93 const fields = hosts[kind];
94 if (schema === undefined || fields === undefined) return hosts;
95 const resolved = yield* resolveSettingsSecrets(resolver, schema, fields);
96 return resolved === fields ? hosts : { ...hosts, [kind]: resolved };
97 },
98);
99​
100const collectUsageLimitSecretReferences = (
101 sources: ServerSettings["usageLimitSources"],
102): ReadonlyArray<SecretReference> =>
103 Object.values(sources).flatMap(({ managementKey }) =>
104 isSecretReference(managementKey) ? [managementKey] : [],
105 );
106​
107const collectDeviceHostSecretReferences = (
108 settings: Pick<ServerSettings, "easDeviceHosts" | "simlockDeviceHosts">,
109): ReadonlyArray<SecretReference> =>
110 [
111 ...settings.easDeviceHosts.map(({ accessToken }) => accessToken),
112 ...settings.simlockDeviceHosts.map(({ agentToken }) => agentToken),
113 ].filter(isSecretReference);
114​
115/** Settings that cannot be read clear every cached secret, so a refresh never keeps a stale one. */
116export const invalidateSettingsSecrets = Effect.fn("SettingsSecrets.invalidate")(function* <E>(
117 resolver: SecretResolver.SecretResolver["Service"],
118 settings: Effect.Effect<
119 Pick<
120 ServerSettings,
121 | "sourceControlHosts"
122 | "usageLimitSources"
123 | "textGenerators"
124 | "easDeviceHosts"
125 | "simlockDeviceHosts"
126 | "cloudSpeechCredentials"
127 >,
128 E
129 >,
130) {
131 const references = yield* settings.pipe(
132 Effect.map((current) => [
133 ...collectSourceControlHostSecretReferences(current.sourceControlHosts),
134 ...collectUsageLimitSecretReferences(current.usageLimitSources),
135 ...Object.values(current.textGenerators).flatMap(({ apiKey }) =>
136 isSecretReference(apiKey) ? [apiKey] : [],
137 ),
138 ...collectDeviceHostSecretReferences(current),
139 ...Object.values(current.cloudSpeechCredentials).filter(isSecretReference),
140 ]),
141 Effect.orElseSucceed(() => undefined),
142 );
143 yield* resolver.invalidate(references);
144});