import type { ConnectionId, ScopeId } from "@executor-js/sdk/core"; import { oauth2ClientSecretSlot } from "./store"; import type { ConfiguredHeaderValue, OpenApiSourceBindingValue } from "./types"; export type BindingRowForCredentialStatus = { readonly slot: string; readonly scopeId: ScopeId; readonly value: OpenApiSourceBindingValue; }; export type SourceForCredentialStatus = { readonly config: { readonly headers?: Record; readonly oauth2?: { readonly securitySchemeName: string; readonly flow: "authorizationCode" | "clientCredentials"; readonly clientIdSlot: string; readonly clientSecretSlot: string | null; readonly connectionSlot: string; }; }; }; const scopeRank = (ranks: ReadonlyMap, scopeId: ScopeId): number => ranks.get(scopeId) ?? Number.MAX_SAFE_INTEGER; export const effectiveBindingForScope = ( rows: readonly BindingRowForCredentialStatus[], slot: string, targetScope: ScopeId, ranks: ReadonlyMap, ): BindingRowForCredentialStatus | null => rows .filter( (row) => row.slot === slot && scopeRank(ranks, row.scopeId) >= scopeRank(ranks, targetScope), ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0] ?? null; const hasSecretBinding = ( rows: readonly BindingRowForCredentialStatus[], slot: string, targetScope: ScopeId, ranks: ReadonlyMap, ) => effectiveBindingForScope(rows, slot, targetScope, ranks)?.value.kind === "secret"; const hasConnectionBinding = ( rows: readonly BindingRowForCredentialStatus[], slot: string, targetScope: ScopeId, ranks: ReadonlyMap, liveConnectionIds?: ReadonlySet, ) => { const binding = effectiveBindingForScope(rows, slot, targetScope, ranks); if (binding?.value.kind !== "connection") return false; return liveConnectionIds ? liveConnectionIds.has(binding.value.connectionId) : true; }; const effectiveClientSecretSlot = (oauth2: { readonly securitySchemeName: string; readonly clientSecretSlot: string | null; }): string => oauth2.clientSecretSlot ?? oauth2ClientSecretSlot(oauth2.securitySchemeName); export function missingCredentialLabels( source: SourceForCredentialStatus, bindings: readonly BindingRowForCredentialStatus[], targetScope: ScopeId, ranks: ReadonlyMap, options?: { readonly liveConnectionIds?: ReadonlySet | readonly ConnectionId[]; }, ): string[] { const missing: string[] = []; const rawLiveConnectionIds = options?.liveConnectionIds; const liveConnectionIds = rawLiveConnectionIds ? rawLiveConnectionIds instanceof Set ? rawLiveConnectionIds : new Set(rawLiveConnectionIds) : undefined; for (const [headerName, value] of Object.entries(source.config.headers ?? {})) { if (typeof value === "string") continue; if (!hasSecretBinding(bindings, value.slot, targetScope, ranks)) { missing.push(headerName); } } const oauth2 = source.config.oauth2; if (!oauth2) return missing; if (!hasSecretBinding(bindings, oauth2.clientIdSlot, targetScope, ranks)) { missing.push("Client ID"); } const clientSecretSlot = effectiveClientSecretSlot(oauth2); if (!hasSecretBinding(bindings, clientSecretSlot, targetScope, ranks)) { missing.push("Client Secret"); } if ( !hasConnectionBinding(bindings, oauth2.connectionSlot, targetScope, ranks, liveConnectionIds) ) { missing.push(oauth2.flow === "clientCredentials" ? "OAuth client connection" : "OAuth sign-in"); } return missing; }