import type { ScopeId, ScopedSecretCredentialInput, SecretBackedValue, } from "@executor-js/sdk/shared"; import { FieldLabel } from "../components/field"; import { HeadersList } from "./headers-list"; import { headerValueToState, headersFromState, QueryParamCredentialValuePreview, type HeaderAuthPreset, type HeaderState, } from "./secret-header-auth"; import type { CredentialTargetScopeOption } from "./credential-target-scope"; import { type ConfiguredCredentialValueLike, type CredentialBindingRefLike, } from "./credential-bindings"; import type { SecretPickerSecret } from "./secret-picker"; export type { SecretBackedValue }; export type QueryParamState = { name: string; secretId: string | null; valueKind?: "secret" | "text"; prefix?: string; literalValue?: string; targetScope?: ScopeId; secretScope?: ScopeId; }; const queryParamPresets: readonly HeaderAuthPreset[] = [ { key: "custom", label: "Query parameter", name: "" }, ]; export type HttpCredentialsState = { headers: HeaderState[]; queryParams: QueryParamState[]; }; export const emptyHttpCredentials = (): HttpCredentialsState => ({ headers: [], queryParams: [], }); export const httpCredentialsFromValues = (input: { readonly headers?: Record | null; readonly queryParams?: Record | null; }): HttpCredentialsState => ({ headers: Object.entries(input.headers ?? {}).map(([name, value]) => headerValueToState(name, value), ), queryParams: Object.entries(input.queryParams ?? {}).map(([name, value]) => { if (typeof value === "string") { return { name, secretId: null, literalValue: value, valueKind: "text" as const }; } return { name, secretId: value.secretId, prefix: value.prefix, valueKind: "secret" as const }; }), }); export const serializeHeaderCredentials = ( headers: readonly HeaderState[], ): Record => headersFromState(headers); export const serializeQueryCredentials = ( queryParams: readonly QueryParamState[], ): Record => { const result: Record = {}; for (const param of queryParams) { const name = param.name.trim(); if (!name) continue; if (param.secretId) { result[name] = { secretId: param.secretId, ...(param.prefix ? { prefix: param.prefix } : {}), }; continue; } if (param.literalValue?.trim()) { result[name] = param.literalValue.trim(); } } return result; }; export const serializeHttpCredentials = ( credentials: HttpCredentialsState, ): { readonly headers: Record; readonly queryParams: Record; } => ({ headers: serializeHeaderCredentials(credentials.headers), queryParams: serializeQueryCredentials(credentials.queryParams), }); export const serializeScopedHeaderCredentials = ( headers: readonly HeaderState[], fallbackTargetScope: ScopeId, ): Record => { const result: Record = {}; for (const header of headers) { const name = header.name.trim(); if (!name || !header.secretId) continue; const targetScope = header.targetScope ?? fallbackTargetScope; result[name] = { secretId: header.secretId, targetScope, ...(header.secretScope ? { secretScopeId: header.secretScope } : {}), ...(header.prefix ? { prefix: header.prefix } : {}), }; } return result; }; export const serializeScopedQueryCredentials = ( queryParams: readonly QueryParamState[], fallbackTargetScope: ScopeId, ): Record => { const result: Record = {}; for (const param of queryParams) { const name = param.name.trim(); if (!name) continue; if (param.secretId) { const targetScope = param.targetScope ?? fallbackTargetScope; result[name] = { secretId: param.secretId, targetScope, ...(param.secretScope ? { secretScopeId: param.secretScope } : {}), ...(param.prefix ? { prefix: param.prefix } : {}), }; continue; } if (param.literalValue?.trim()) { result[name] = param.literalValue.trim(); } } return result; }; export const serializeScopedHttpCredentials = ( credentials: HttpCredentialsState, fallbackTargetScope: ScopeId, ) => ({ headers: serializeScopedHeaderCredentials(credentials.headers, fallbackTargetScope), queryParams: serializeScopedQueryCredentials(credentials.queryParams, fallbackTargetScope), }); export type HttpConfigureCredentialInput = | string | { readonly kind: "text"; readonly text: string; readonly prefix?: string; } | { readonly kind: "secret"; readonly secretId: string; readonly secretScope?: ScopeId; readonly prefix?: string; }; export const serializeConfigureHeaderCredentials = ( headers: readonly HeaderState[], fallbackSecretScope: ScopeId, ): Record => { const result: Record = {}; for (const header of headers) { const name = header.name.trim(); if (!name) continue; if (header.valueKind === "text") { if (header.literalValue?.trim()) { result[name] = header.literalValue.trim(); } continue; } if (!header.secretId) continue; result[name] = { kind: "secret", secretId: header.secretId, secretScope: header.secretScope ?? fallbackSecretScope, ...(header.prefix ? { prefix: header.prefix } : {}), }; } return result; }; export const serializeConfigureQueryCredentials = ( queryParams: readonly QueryParamState[], fallbackSecretScope: ScopeId, ): Record => { const result: Record = {}; for (const param of queryParams) { const name = param.name.trim(); if (!name) continue; if (param.secretId) { result[name] = { kind: "secret", secretId: param.secretId, secretScope: param.secretScope ?? fallbackSecretScope, ...(param.prefix ? { prefix: param.prefix } : {}), }; continue; } if (param.literalValue?.trim()) { result[name] = param.literalValue.trim(); } } return result; }; export const serializeConfigureHttpCredentials = ( credentials: HttpCredentialsState, fallbackSecretScope: ScopeId, ) => ({ headers: serializeConfigureHeaderCredentials(credentials.headers, fallbackSecretScope), queryParams: serializeConfigureQueryCredentials(credentials.queryParams, fallbackSecretScope), }); export type HttpTemplateCredentialInput = | string | { readonly kind: "secret"; readonly prefix?: string }; export const serializeTemplateHeaderCredentials = ( headers: readonly HeaderState[], ): Record => { const result: Record = {}; for (const header of headers) { const name = header.name.trim(); if (!name) continue; if (header.valueKind === "text") { if (header.literalValue?.trim()) { result[name] = header.literalValue.trim(); } continue; } if (!header.secretId) continue; result[name] = { kind: "secret", ...(header.prefix ? { prefix: header.prefix } : {}), }; } return result; }; export const serializeTemplateQueryCredentials = ( queryParams: readonly QueryParamState[], ): Record => { const result: Record = {}; for (const param of queryParams) { const name = param.name.trim(); if (!name) continue; if (param.secretId) { result[name] = { kind: "secret", ...(param.prefix ? { prefix: param.prefix } : {}), }; continue; } if (param.literalValue?.trim()) { result[name] = param.literalValue.trim(); } } return result; }; const bindingBySlot = ( bindings: readonly CredentialBindingRefLike[], ): ReadonlyMap => new Map(bindings.map((binding) => [binding.slotKey, binding])); const headerFromConfiguredCredential = ( name: string, value: ConfiguredCredentialValueLike, bindings: ReadonlyMap, ): HeaderState | null => { if (typeof value === "string") { return headerValueToState(name, value); } const binding = bindings.get(value.slot); if (binding?.value.kind === "secret") { return { ...headerValueToState(name, { secretId: binding.value.secretId, prefix: value.prefix, }), targetScope: binding.scopeId, secretScope: binding.value.secretScopeId, }; } if (binding?.value.kind === "text") { return headerValueToState(name, binding.value.text); } return null; }; const queryParamFromConfiguredCredential = ( name: string, value: ConfiguredCredentialValueLike, bindings: ReadonlyMap, ): QueryParamState | null => { if (typeof value === "string") { return { name, secretId: null, literalValue: value, valueKind: "text" }; } const binding = bindings.get(value.slot); if (binding?.value.kind === "secret") { return { name, secretId: binding.value.secretId, valueKind: "secret", prefix: value.prefix, targetScope: binding.scopeId, secretScope: binding.value.secretScopeId, }; } if (binding?.value.kind === "text") { return { name, secretId: null, literalValue: binding.value.text, valueKind: "text" }; } return null; }; export const httpCredentialsFromConfiguredCredentialBindings = (input: { readonly headers?: Record | null; readonly queryParams?: Record | null; readonly bindings: readonly CredentialBindingRefLike[]; }): HttpCredentialsState => { const bindings = bindingBySlot(input.bindings); return { headers: Object.entries(input.headers ?? {}).flatMap(([name, value]) => { const state = headerFromConfiguredCredential(name, value, bindings); return state ? [state] : []; }), queryParams: Object.entries(input.queryParams ?? {}).flatMap(([name, value]) => { const state = queryParamFromConfiguredCredential(name, value, bindings); return state ? [state] : []; }), }; }; export const serializeTemplateHttpCredentials = (credentials: HttpCredentialsState) => ({ headers: serializeTemplateHeaderCredentials(credentials.headers), queryParams: serializeTemplateQueryCredentials(credentials.queryParams), }); export const httpCredentialsValid = (credentials: HttpCredentialsState): boolean => credentials.headers.every((header) => { if (!header.name.trim()) return false; return header.valueKind === "text" ? Boolean(header.literalValue?.trim()) : Boolean(header.secretId); }) && credentials.queryParams.every((param) => { if (!param.name.trim()) return false; return Boolean(param.secretId || param.literalValue?.trim()); }); export function HttpCredentialsEditor(props: { readonly credentials: HttpCredentialsState; readonly onChange: (credentials: HttpCredentialsState) => void; readonly existingSecrets: readonly SecretPickerSecret[]; readonly sourceName?: string; readonly targetScope: ScopeId; readonly credentialScopeOptions?: readonly CredentialTargetScopeOption[]; readonly bindingScopeOptions?: readonly CredentialTargetScopeOption[]; readonly sections?: { readonly headers?: boolean; readonly queryParams?: boolean; }; readonly labels?: { readonly headers?: string; readonly queryParams?: string; }; readonly headerPresets?: readonly HeaderAuthPreset[]; }) { const showHeaders = props.sections?.headers ?? true; const showQueryParams = props.sections?.queryParams ?? true; return (
{showHeaders && (
{props.labels?.headers ?? "Headers"} props.onChange({ ...props.credentials, headers })} existingSecrets={props.existingSecrets} sourceName={props.sourceName} targetScope={props.targetScope} credentialScopeOptions={props.credentialScopeOptions} bindingScopeOptions={props.bindingScopeOptions} presets={props.headerPresets} />
)} {showQueryParams && (
{props.labels?.queryParams ?? "Query parameters"} props.onChange({ ...props.credentials, queryParams })} existingSecrets={props.existingSecrets} sourceName={props.sourceName} targetScope={props.targetScope} credentialScopeOptions={props.credentialScopeOptions} bindingScopeOptions={props.bindingScopeOptions} presets={queryParamPresets} emptyLabel="No query parameters" addLabel="Add query parameter" addAriaLabel="Add query parameter" rowCopy={{ rowLabel: "Query parameter", namePlaceholder: "token", }} rowPreviewComponent={QueryParamCredentialValuePreview} />
)}
); }