1/**
2 * Which source control host settings fields hold secrets, read from each built-in host's client
3 * definition. `serverSettings` keeps those values in the secret store and redacts them for
4 * clients, the same way for every host.
5 *
6 * A secret field holds a string, or a string per server host such as GitHub's `tokens`.
7 *
8 * @module sourceControl/sourceControlHostSecrets
9 */
10import { secretSettingsFields } from "@t3tools/contracts";
11import * as AzureDevOps from "@t3tools/source-control-azure-devops/client/definition";
12import * as Bitbucket from "@t3tools/source-control-bitbucket/client/definition";
13import type { SourceControlClientDefinition } from "@t3tools/source-control-core/client/definition";
14import * as Forgejo from "@t3tools/source-control-forgejo/client/definition";
15import * as GitCafe from "@t3tools/source-control-gitcafe/client/definition";
16import * as GitHub from "@t3tools/source-control-github/client/definition";
17import * as GitLab from "@t3tools/source-control-gitlab/client/definition";
18
19const definitions: ReadonlyArray<SourceControlClientDefinition> = [
20 GitHub.definition,
21 GitLab.definition,
22 AzureDevOps.definition,
23 Bitbucket.definition,
24 Forgejo.definition,
25 GitCafe.definition,
26];
27
28/** Secret field names per host kind; a host with none is absent. */
29export const SOURCE_CONTROL_HOST_SECRET_FIELDS: ReadonlyMap<
30 string,
31 ReadonlyArray<string>
32> = new Map(
33 definitions.flatMap((definition) => {
34 const fields = definition.settings ? secretSettingsFields(definition.settings) : [];
35 return fields.length > 0 ? [[definition.kind, fields] as const] : [];
36 }),
37);
38
39/** One stored secret: a host's field, and the server host it is for when the field is per host. */
40export interface SourceControlHostSecretSlot {
41 readonly kind: string;
42 readonly field: string;
43 readonly serverHost: string | null;
44}
45
46/** The secret store entry for one slot; names stay stable across releases. */
47export function sourceControlHostSecretName(slot: SourceControlHostSecretSlot): string {
48 const encode = (value: string) => Buffer.from(value, "utf8").toString("base64url");
49 return slot.serverHost === null
50 ? `source-control-${encode(slot.kind)}-${encode(slot.field)}`
51 : `source-control-${encode(slot.kind)}-${encode(slot.field)}-${encode(slot.serverHost.trim().toLowerCase())}`;
52}
53
54/** The string values a secret field holds, keyed by server host, or `null` for a plain string. */
55export function readSecretFieldValues(
56 value: unknown,
57): ReadonlyArray<readonly [serverHost: string | null, value: string]> {
58 if (typeof value === "string") return [[null, value]];
59 if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
60 return Object.entries(value).flatMap(([serverHost, entry]) =>
61 typeof entry === "string" ? [[serverHost.trim().toLowerCase(), entry] as const] : [],
62 );
63}
64
65/** A secret field's value rebuilt from per-slot values, in the shape it was read from. */
66export function writeSecretFieldValues(
67 original: unknown,
68 values: ReadonlyArray<readonly [serverHost: string | null, value: string]>,
69): unknown {
70 if (typeof original === "string") return values[0]?.[1] ?? "";
71 return Object.fromEntries(values.map(([serverHost, value]) => [serverHost ?? "", value]));
72}