222 lines · 9.0 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// OpenAPI ↔ generic auth-method converters.//// The generic Accounts hub speaks in plugin-agnostic `AuthMethod` /// `Placement` values (`@executor-js/react/lib/auth-placements`). The OpenAPI// plugin stores auth as the HTTP-transport-specific `APIKeyAuthentication`// template (header/query slots templated with `variable("token")`). These two// converters bridge the wire template to/from the generic placement model, so// they live with the OpenAPI plugin — they touch the openapi sdk// `Authentication` types and would pull transport specifics into core.// --------------------------------------------------------------------------- import { AuthTemplateSlug } from "@executor-js/sdk/shared";import type { AuthMethod, Placement } from "@executor-js/react/lib/auth-placements";import type { AuthTemplateEditorValue } from "@executor-js/react/components/auth-template-editor"; import {  TOKEN_VARIABLE,  variable,  type APIKeyAuthentication,  type Authentication,  type AuthenticationTemplateValue,} from "../sdk/types"; // ---------------------------------------------------------------------------// Template value → placement prefix.//// A header/query slot serializes the credential as `name -> [prefix, token]`,// where `prefix` is the leading literal string before the `variable("token")`// part. A bare `[token]` (or string-only) value has an empty prefix.// --------------------------------------------------------------------------- const isVariablePart = (part: string | { readonly type: "variable"; readonly name: string }) =>  typeof part !== "string" && part.type === "variable"; /** Extract the literal prefix preceding the credential variable, plus the *  variable name that placement renders from (`token` for single-input methods, *  a distinct name per input for multi-input ones). */const parseTemplateValue = (  value: AuthenticationTemplateValue,): { readonly prefix: string; readonly variable: string } => {  if (typeof value === "string") return { prefix: "", variable: TOKEN_VARIABLE };  const parts: string[] = [];  for (const part of value) {    if (isVariablePart(part)) {      return { prefix: parts.join(""), variable: (part as { readonly name: string }).name };    }    if (typeof part === "string") parts.push(part);  }  return { prefix: parts.join(""), variable: TOKEN_VARIABLE };}; export const placementsFromApiKey = (template: APIKeyAuthentication): Placement[] => {  const placements: Placement[] = [];  for (const [name, value] of Object.entries(template.headers ?? {})) {    const { prefix, variable } = parseTemplateValue(value);    placements.push({ carrier: "header", name, prefix, variable });  }  for (const [name, value] of Object.entries(template.queryParams ?? {})) {    const { prefix, variable } = parseTemplateValue(value);    placements.push({ carrier: "query", name, prefix, variable });  }  return placements;}; // ---------------------------------------------------------------------------// Templates → generic methods.// --------------------------------------------------------------------------- const labelForApiKey = (slug: string, placements: readonly Placement[]): string => {  const first = placements[0];  if (first) return `API key (${first.name || (first.carrier === "header" ? "header" : "query")})`;  return `API key (${slug})`;}; /** Map each stored auth template to a generic `AuthMethod`. */export function authMethodsFromConfig(templates: readonly Authentication[]): AuthMethod[] {  return templates.map((template: Authentication): AuthMethod => {    const slug = String(template.slug);    const source: "spec" | "custom" = slug.startsWith("custom_") ? "custom" : "spec";    if (template.type === "oauth") {      return {        id: slug,        label: "OAuth2",        kind: "oauth",        source,        template: AuthTemplateSlug.make(slug),        placements: [],        // Carry the integration's declared endpoints/scopes so the        // client-registration form pre-fills them.        oauth: {          authorizationUrl: template.authorizationUrl,          tokenUrl: template.tokenUrl,          scopes: template.scopes,        },      };    }    const placements = placementsFromApiKey(template);    return {      id: slug,      label: labelForApiKey(slug, placements),      kind: "apikey",      source,      template: AuthTemplateSlug.make(slug),      placements,    };  });} // ---------------------------------------------------------------------------// Generic placements → apiKey template (inverse).//// Each placement becomes a header/query slot whose value is// `prefix ? [prefix, variable("token")] : [variable("token")]`. A custom method// may omit `slug`; the backend backfills `custom_<id>`.// --------------------------------------------------------------------------- /** Slugify a placement name into a variable identifier: `DD-API-KEY` → *  `dd_api_key`. */const slugifyVariable = (name: string): string =>  name    .toLowerCase()    .replace(/[^a-z0-9]+/g, "_")    .replace(/^_+|_+$/g, ""); /** Assign an input variable to each (named) placement. A lone input is the *  canonical `token`; multiple inputs each get their own distinct variable so a *  connection can carry a different value per location. An explicit *  `placement.variable` (from a round-trip) is honored. */const assignVariables = (placements: readonly Placement[]): Map<Placement, string> => {  const named = placements.filter((p) => p.name);  const out = new Map<Placement, string>();  if (named.length <= 1) {    for (const p of named) out.set(p, p.variable ?? TOKEN_VARIABLE);    return out;  }  const taken = new Set<string>();  for (const p of named) {    const base = slugifyVariable(p.name) || "input";    let candidate = p.variable ?? base;    let n = 2;    while (taken.has(candidate)) candidate = `${base}_${n++}`;    taken.add(candidate);    out.set(p, candidate);  }  return out;}; const valueFromPlacement = (placement: Placement, varName: string): AuthenticationTemplateValue =>  placement.prefix ? [placement.prefix, variable(varName)] : [variable(varName)]; /** Build an `APIKeyAuthentication` template from generic placements. When *  `slug` is omitted the backend assigns a `custom_<id>` slug. */export function templateFromPlacements(  placements: readonly Placement[],  slug?: string,): APIKeyAuthentication {  const variables = assignVariables(placements);  const headers: Record<string, AuthenticationTemplateValue> = {};  const queryParams: Record<string, AuthenticationTemplateValue> = {};  for (const placement of placements) {    if (!placement.name) continue;    const varName = variables.get(placement) ?? TOKEN_VARIABLE;    if (placement.carrier === "header") {      headers[placement.name] = valueFromPlacement(placement, varName);    } else {      queryParams[placement.name] = valueFromPlacement(placement, varName);    }  }  return {    slug: AuthTemplateSlug.make(slug ?? ""),    type: "apiKey",    ...(Object.keys(headers).length > 0 ? { headers } : {}),    ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}),  };} // ---------------------------------------------------------------------------// Stored `Authentication` ⇆ generic `AuthTemplateEditorValue`.//// The shared add-time `AuthTemplateEditor` edits a plugin-agnostic value; these// bridge it to/from the OpenAPI wire template so a spec-detected method seeds an// editable default and the user's edits round-trip back to `Authentication[]`// on submit. apiKey → placements; oauth → endpoints + scopes.// --------------------------------------------------------------------------- /** Convert one stored `Authentication` template into a generic editor value. */export function editorValueFromAuthentication(template: Authentication): AuthTemplateEditorValue {  if (template.type === "oauth") {    return {      kind: "oauth",      authorizationUrl: template.authorizationUrl ?? "",      tokenUrl: template.tokenUrl ?? "",      scopes: template.scopes ?? [],    };  }  return { kind: "apikey", placements: placementsFromApiKey(template) };} /** Build an `OAuthAuthentication` template from a generic oauth editor value. */const oauthTemplateFromEditorValue = (  value: Extract<AuthTemplateEditorValue, { kind: "oauth" }>,  slug?: string,): Authentication => ({  slug: AuthTemplateSlug.make(slug ?? ""),  type: "oauth",  authorizationUrl: value.authorizationUrl,  tokenUrl: value.tokenUrl,  scopes: [...value.scopes],}); /** Convert one generic editor value back into a stored `Authentication`, or *  `null` for `none` (no method to register). The optional `slug` names the *  template; when omitted the backend backfills `custom_<id>`. */export function authenticationFromEditorValue(  value: AuthTemplateEditorValue,  slug?: string,): Authentication | null {  if (value.kind === "none") return null;  if (value.kind === "oauth") return oauthTemplateFromEditorValue(value, slug);  return templateFromPlacements(value.placements, slug);}