188 lines · 8.0 KB · TypeScript
RawDownload
import { Schema } from "effect";import type { AuthMethodDescriptor, AuthPlacementDescriptor } from "../integration"; // ---------------------------------------------------------------------------// The shared HTTP auth-method vocabulary for protocol plugins (openapi,// graphql, mcp). An integration declares a LIST of auth methods; a connection// binds one by its `slug` (`connection.template`) and supplies one credential// value per `variable` the method's placements reference (D11: core resolves// `values: Record<variable, string|null>`, the plugin renders them onto the// request).//// Core itself never imports this module — core stays carrier-agnostic (a// connection could be a CLI login or a DB URL). It ships as the// `@executor-js/sdk/http-auth` subpath purely as a home: composition, not// location, is what keeps headers/query params an HTTP-plugin concern.//// OAuth methods are NOT modeled here — their config genuinely differs per// plugin (openapi stores endpoints+scopes, graphql an optional header// override, mcp discovers everything at connect time). Each plugin's method// union is `NoneAuthMethod | ApiKeyAuthMethod | <its own oauth variant>`.// OAuth-refreshed connections resolve only the `token` input, so oauth// values must never be mixed into a placements method.// --------------------------------------------------------------------------- /** The canonical input variable of single-credential methods. A placement *  with no `variable` renders from it, and core resolves a connection created *  with a bare `{ value }` into `{ token: value }`. */export const TOKEN_VARIABLE = "token"; export const AuthCarrier = Schema.Literals(["header", "query"]);export type AuthCarrier = typeof AuthCarrier.Type; /** One spot on the outbound request a method writes to. * *  Credential placement: renders `prefix + values[variable ?? "token"]`. *  Two placements naming the same variable share one credential input; a *  placement naming its own variable gets its own input (e.g. Datadog's two *  keys). * *  Literal placement (`literal` set): renders the literal verbatim and *  references no credential — static values a method carries alongside its *  credential (e.g. a fixed version header). */export const AuthPlacement = Schema.Struct({  carrier: AuthCarrier,  /** Header name (e.g. `Authorization`) or query-param name (e.g. `token`). */  name: Schema.String,  /** Literal prepended to the credential value, e.g. `Bearer `. */  prefix: Schema.optional(Schema.String),  /** The credential input this placement renders from. Absent ⇒ `token`. */  variable: Schema.optional(Schema.String),  /** Render this exact value instead of a credential. */  literal: Schema.optional(Schema.String),});export type AuthPlacement = typeof AuthPlacement.Type; export const ApiKeyAuthMethod = Schema.Struct({  slug: Schema.String,  kind: Schema.Literal("apikey"),  /** Display label; derived from the first placement when absent. */  label: Schema.optional(Schema.String),  placements: Schema.Array(AuthPlacement),});export type ApiKeyAuthMethod = typeof ApiKeyAuthMethod.Type; /** An open integration — connections carry no credential. */export const NoneAuthMethod = Schema.Struct({  slug: Schema.String,  kind: Schema.Literal("none"),});export type NoneAuthMethod = typeof NoneAuthMethod.Type; // ---------------------------------------------------------------------------// Rendering// --------------------------------------------------------------------------- export interface RenderedAuthPlacements {  readonly headers: Record<string, string>;  readonly queryParams: Record<string, string>;} const renderPlacementValue = (  placement: AuthPlacement,  values: Record<string, string | null>,): string | null => {  if (placement.literal !== undefined) return placement.literal;  const value = values[placement.variable ?? TOKEN_VARIABLE];  if (value == null) return null;  return placement.prefix ? `${placement.prefix}${value}` : value;}; /** Render a method's placements with a connection's resolved values. Total: *  a credential placement whose variable resolved to nothing is skipped — *  callers own their missing-value policy (fail the invocation, dial *  unauthenticated, …) and should gate on `requiredPlacementVariables`. */export const renderAuthPlacements = (  placements: readonly AuthPlacement[],  values: Record<string, string | null>,): RenderedAuthPlacements => {  const headers: Record<string, string> = {};  const queryParams: Record<string, string> = {};  for (const placement of placements) {    const rendered = renderPlacementValue(placement, values);    if (rendered === null) continue;    if (placement.carrier === "header") headers[placement.name] = rendered;    else queryParams[placement.name] = rendered;  }  return { headers, queryParams };}; /** The distinct credential inputs a method's placements reference — what a *  connection must supply. Literal placements reference none. */export const requiredPlacementVariables = (  placements: readonly AuthPlacement[],): readonly string[] => {  const names = new Set<string>();  for (const placement of placements) {    if (placement.literal !== undefined) continue;    names.add(placement.variable ?? TOKEN_VARIABLE);  }  return [...names];}; /** The conventional rendering of an OAuth access token, as a placement — for *  plugins whose oauth method applies the token as a plain (possibly *  customized) header. The token is always the `token` input. */export const oauthBearerPlacement = (header?: string, prefix?: string): AuthPlacement => ({  carrier: "header",  name: header ?? "Authorization",  prefix: prefix ?? "Bearer ",}); // ---------------------------------------------------------------------------// Slug normalization — methods are input without slugs from UIs/agents; every// stored method needs a stable one (`connection.template` binds by it).// --------------------------------------------------------------------------- /** Assign each method a stable slug: a caller-provided one wins, otherwise *  `defaultSlugFor(method)`, suffixed `_2`, `_3`, … on collision. */export const normalizeAuthMethodSlugs = <T extends { readonly slug?: string | undefined }>(  methods: readonly T[],  defaultSlugFor: (method: T) => string,): readonly (T & { readonly slug: string })[] => {  const taken = new Set<string>();  return methods.map((method: T): T & { readonly slug: string } => {    const requested = method.slug?.trim() || defaultSlugFor(method);    let slug = requested;    for (let n = 2; taken.has(slug); n += 1) slug = `${requested}_${n}`;    taken.add(slug);    return { ...method, slug };  });}; // ---------------------------------------------------------------------------// Catalog projection — the plugin-agnostic `AuthMethodDescriptor` every// plugin's `describeAuthMethods` emits (the descriptor TYPE stays in// @executor-js/sdk: core carries it as opaque catalog metadata).// --------------------------------------------------------------------------- export const apiKeyMethodLabel = (method: ApiKeyAuthMethod): string => {  if (method.label !== undefined && method.label.trim().length > 0) return method.label;  const first = method.placements.find(    (placement: AuthPlacement) => placement.name.trim().length > 0,  );  return first ? `API key (${first.name})` : `API key (${method.slug})`;}; const describePlacement = (placement: AuthPlacement): AuthPlacementDescriptor => ({  carrier: placement.carrier,  name: placement.name,  prefix: placement.prefix ?? "",  ...(placement.variable !== undefined ? { variable: placement.variable } : {}),  ...(placement.literal !== undefined ? { literal: placement.literal } : {}),}); export const describeApiKeyAuthMethod = (method: ApiKeyAuthMethod): AuthMethodDescriptor => ({  id: method.slug,  label: apiKeyMethodLabel(method),  kind: "apikey",  template: method.slug,  placements: method.placements.map(describePlacement),}); export const describeNoneAuthMethod = (slug: string): AuthMethodDescriptor => ({  id: slug,  label: "No authentication",  kind: "none",  template: slug,});