| 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 | */ |
| 10 | import { secretSettingsFields } from "@t3tools/contracts"; |
| 11 | import * as AzureDevOps from "@t3tools/source-control-azure-devops/client/definition"; |
| 12 | import * as Bitbucket from "@t3tools/source-control-bitbucket/client/definition"; |
| 13 | import type { SourceControlClientDefinition } from "@t3tools/source-control-core/client/definition"; |
| 14 | import * as Forgejo from "@t3tools/source-control-forgejo/client/definition"; |
| 15 | import * as GitCafe from "@t3tools/source-control-gitcafe/client/definition"; |
| 16 | import * as GitHub from "@t3tools/source-control-github/client/definition"; |
| 17 | import * as GitLab from "@t3tools/source-control-gitlab/client/definition"; |
| 18 | |
| 19 | const 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. */ |
| 29 | export 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. */ |
| 40 | export 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. */ |
| 47 | export 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. */ |
| 55 | export 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. */ |
| 66 | export 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 | } |