// 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, 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), );