152 lines · 5.8 KB · TypeScript
RawDownload
// Spec-detected auth → stored `Authentication` templates, shared by every add// path. The React add flow derives templates from the preview before calling// `addSpec`; `addSpec` itself falls back to the same derivation when the// caller omits `authenticationTemplate` (the agentic/API path has no client// to do it). One implementation so the web UI and headless callers cannot// drift: an integration added over MCP gets the same auth methods the add// page would have produced.import * as Option from "effect/Option"; import { AuthTemplateSlug, type OAuthAuthentication } from "@executor-js/sdk/shared"; import type { HeaderPreset, OAuth2Preset, SpecPreview } from "./preview";import type { APIKeyAuthentication, Authentication } from "./types";import { resolveServerUrl } from "./openapi-utils"; // ---------------------------------------------------------------------------// OpenAPI url helpers — specs sometimes ship relative OAuth endpoints; resolve// them against the chosen base URL so the stored auth template is absolute.// --------------------------------------------------------------------------- export function resolveOAuthUrl(url: string, baseUrl: string): string {  if (!url) return url;  // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes provider metadata URLs  try {    new URL(url);    return url;  } catch {    if (!baseUrl) return url;    // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor resolves relative provider metadata URLs    try {      return new URL(url, baseUrl).toString();    } catch {      return url;    }  }} const standardOidcIdentityScopes = ["openid", "email", "profile"] as const; const identityScopesForPreset = (  identityScopes: OAuth2Preset["identityScopes"],): readonly string[] => {  if (identityScopes === false) return [];  return identityScopes === "auto" ? standardOidcIdentityScopes : identityScopes;}; export const resolvedOAuthScopes = (  apiScopes: Iterable<string>,  identityScopes: OAuth2Preset["identityScopes"],): string[] => {  const merged = new Set(apiScopes);  for (const scope of identityScopesForPreset(identityScopes)) merged.add(scope);  return [...merged];}; // ---------------------------------------------------------------------------// Auth-template builders — turn a preview preset into the integration's stored// `Authentication` template (v2). The header preset becomes an `apiKey` template// whose secret header value renders the resolved credential via `variable(token)`;// the oauth2 preset becomes an `oauth` template carrying the provider endpoints.// --------------------------------------------------------------------------- const headerPrefix = (preset: HeaderPreset, headerName: string): string | undefined => {  const label = preset.label.toLowerCase();  if (headerName.toLowerCase() === "authorization") {    if (label.includes("bearer")) return "Bearer ";    if (label.includes("basic")) return "Basic ";  }  return undefined;}; const apiKeyTemplateFromHeaderPreset = (  preset: HeaderPreset,  slug: AuthTemplateSlug,): APIKeyAuthentication => ({  slug,  kind: "apikey",  // Every secret header shares the one credential input (the canonical  // `token`, stored as an absent placement variable).  placements: preset.secretHeaders.map((headerName) => {    const prefix = headerPrefix(preset, headerName);    return { carrier: "header" as const, name: headerName, ...(prefix ? { prefix } : {}) };  }),}); const oauthTemplateFromPreset = (  preset: OAuth2Preset,  baseUrl: string,  slug: AuthTemplateSlug,  scopes: readonly string[],): OAuthAuthentication => ({  slug,  kind: "oauth2",  authorizationUrl: resolveOAuthUrl(    Option.getOrElse(preset.authorizationUrl, () => ""),    baseUrl,  ),  tokenUrl: resolveOAuthUrl(preset.tokenUrl, baseUrl),  scopes: [...scopes],}); // ---------------------------------------------------------------------------// All spec-detected auth methods → the union of stored `Authentication`// templates. Header presets become apiKey templates; each oauth2 preset becomes// an oauth template (with its declared API scopes plus, for auth-code flows,// the standard identity scopes). Slugs stay deterministic per method so the// stored template is stable across previews of the same spec.// --------------------------------------------------------------------------- export const detectedAuthenticationTemplates = (  headerPresets: readonly HeaderPreset[],  oauth2Presets: readonly OAuth2Preset[],  baseUrl: string,): readonly Authentication[] => {  const templates: Authentication[] = [];  headerPresets.forEach((preset, index) => {    templates.push(      apiKeyTemplateFromHeaderPreset(preset, AuthTemplateSlug.make(`apikey-${index}`)),    );  });  for (const preset of oauth2Presets) {    const scopes = resolvedOAuthScopes(Object.keys(preset.scopes), preset.identityScopes);    templates.push(      oauthTemplateFromPreset(        preset,        baseUrl,        AuthTemplateSlug.make(`oauth-${preset.securitySchemeName}`),        scopes,      ),    );  }  return templates;}; export const firstBaseUrlForPreview = (preview: SpecPreview): string => {  const firstServer = preview.servers[0];  return firstServer    ? resolveServerUrl(firstServer.url, Option.getOrUndefined(firstServer.variables), {})    : "";}; /** The fallback `addSpec` uses when no explicit template was passed: every *  spec-detected method, resolved against the integration's base URL. */export const deriveAuthenticationTemplateFromPreview = (  preview: SpecPreview,  baseUrl: string | undefined,): readonly Authentication[] =>  detectedAuthenticationTemplates(    preview.headerPresets,    preview.oauth2Presets,    baseUrl ?? firstBaseUrlForPreview(preview),  );