import { isSecretReference, secretReferencesInSettings, secretSettingsFields, type SecretReference, type ServerSettings, } from "@t3tools/contracts"; import type * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver"; import * as Effect from "effect/Effect"; import { resolveSettingsSecrets } from "../secrets/settingsSecretValue.ts"; import { sourceControlDefinitions } from "./sourceControlDefinitions.ts"; export const SOURCE_CONTROL_HOST_SECRET_FIELDS: ReadonlyMap< string, ReadonlyArray > = new Map( sourceControlDefinitions.definitions.flatMap((definition) => { const fields = definition.settings ? secretSettingsFields(definition.settings) : []; return fields.length > 0 ? [[definition.kind, fields] as const] : []; }), ); export interface SourceControlHostSecretSlot { readonly kind: string; readonly field: string; readonly serverHost: string | null; } /** The secret store entry for one slot; names stay stable across releases. */ export function sourceControlHostSecretName(slot: SourceControlHostSecretSlot): string { const encode = (value: string) => Buffer.from(value, "utf8").toString("base64url"); return slot.serverHost === null ? `source-control-${encode(slot.kind)}-${encode(slot.field)}` : `source-control-${encode(slot.kind)}-${encode(slot.field)}-${encode(slot.serverHost.trim().toLowerCase())}`; } export function readSecretFieldValues( value: unknown, ): ReadonlyArray { if (typeof value === "string") return [[null, value]]; if (isSecretReference(value)) return []; if (value === null || typeof value !== "object" || Array.isArray(value)) return []; return Object.entries(value).flatMap(([serverHost, entry]) => typeof entry === "string" ? [[serverHost.trim().toLowerCase(), entry] as const] : [], ); } export function writeSecretFieldValues( original: unknown, values: ReadonlyArray, ): unknown { if (typeof original === "string") return values[0]?.[1] ?? ""; if (isSecretReference(original)) return original; const sources = original !== null && typeof original === "object" && !Array.isArray(original) ? Object.entries(original).flatMap(([serverHost, entry]) => isSecretReference(entry) ? [[serverHost.trim().toLowerCase(), entry] as const] : [], ) : []; return Object.fromEntries([ ...sources, ...values.map(([serverHost, value]) => [serverHost ?? "", value] as const), ]); } type SourceControlHostsSettings = Readonly>>>; const hostSettingsSchemas = new Map( sourceControlDefinitions.definitions.flatMap((definition) => definition.settings ? [[definition.kind as string, definition.settings] as const] : [], ), ); /** Every secret provider reference held in any host kind's credential fields. */ export function collectSourceControlHostSecretReferences( hosts: SourceControlHostsSettings, ): ReadonlyArray { return [...hostSettingsSchemas].flatMap(([kind, schema]) => secretReferencesInSettings(schema, hosts[kind]).map(({ reference }) => reference), ); } /** Replaces each secret provider reference in one host kind's credential fields with the secret it names. */ export const resolveSourceControlHostSecrets = Effect.fn("SourceControlHost.resolveSecrets")( function* ( resolver: SecretResolver.SecretResolver["Service"], hosts: SourceControlHostsSettings, kind: string, ) { const schema = hostSettingsSchemas.get(kind); const fields = hosts[kind]; if (schema === undefined || fields === undefined) return hosts; const resolved = yield* resolveSettingsSecrets(resolver, schema, fields); return resolved === fields ? hosts : { ...hosts, [kind]: resolved }; }, ); const collectUsageLimitSecretReferences = ( sources: ServerSettings["usageLimitSources"], ): ReadonlyArray => Object.values(sources).flatMap(({ managementKey }) => isSecretReference(managementKey) ? [managementKey] : [], ); const collectDeviceHostSecretReferences = ( settings: Pick, ): ReadonlyArray => [ ...settings.easDeviceHosts.map(({ accessToken }) => accessToken), ...settings.simlockDeviceHosts.map(({ agentToken }) => agentToken), ].filter(isSecretReference); /** Settings that cannot be read clear every cached secret, so a refresh never keeps a stale one. */ export const invalidateSettingsSecrets = Effect.fn("SettingsSecrets.invalidate")(function* ( resolver: SecretResolver.SecretResolver["Service"], settings: Effect.Effect< Pick< ServerSettings, | "sourceControlHosts" | "usageLimitSources" | "textGenerators" | "easDeviceHosts" | "simlockDeviceHosts" | "cloudSpeechCredentials" >, E >, ) { const references = yield* settings.pipe( Effect.map((current) => [ ...collectSourceControlHostSecretReferences(current.sourceControlHosts), ...collectUsageLimitSecretReferences(current.usageLimitSources), ...Object.values(current.textGenerators).flatMap(({ apiKey }) => isSecretReference(apiKey) ? [apiKey] : [], ), ...collectDeviceHostSecretReferences(current), ...Object.values(current.cloudSpeechCredentials).filter(isSecretReference), ]), Effect.orElseSucceed(() => undefined), ); yield* resolver.invalidate(references); });