848 lines · 32.0 KB · TypeScript
RawDownload
import { Effect, Option, Schema } from "effect";import type { Layer } from "effect";import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import { AuthTemplateSlug } from "@executor-js/sdk/core";import { AuthenticationSchema, type OpenApiIntegrationConfig } from "../../sdk/config";import { OpenApiParseError } from "../../sdk/errors";import {  parseEntry,  parseHead,  parseSmallComponents,  structuralSplit,  type KeepPathItem,  type SpecStructure,} from "../../sdk/split";import type { Authentication } from "../../sdk/types"; import {  fetchMicrosoftGraphSlice,  microsoftGraphPresetIdsForSliceAsset,  microsoftGraphSliceAssetFromUrl,} from "./slices";import {  MICROSOFT_AUTHORIZATION_URL,  MICROSOFT_AUTH_TEMPLATE_SLUG,  MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL,  MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG,  MICROSOFT_DELEGATED_AUTH_LABEL,  MICROSOFT_GRAPH_BASE_SCOPES,  MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES,  MICROSOFT_GRAPH_DELEGATED_DEFAULT_SCOPES,  MICROSOFT_GRAPH_DEFAULT_PRESET_IDS,  MICROSOFT_GRAPH_IDENTITY_SCOPE,  MICROSOFT_GRAPH_OPENAPI_URL,  MICROSOFT_GRAPH_PERMISSIONS_REFERENCE_URL,  MICROSOFT_TOKEN_URL,  microsoftGraphExactPathsForPresetIds,  microsoftGraphPathPrefixesForPresetIds,  microsoftGraphPresetIdsCoverFullGraph,  microsoftGraphScopesForPresetIds,  microsoftGraphTagPrefixesForPresetIds,} from "./presets"; export interface MicrosoftGraphSelectionInput {  readonly presetIds?: readonly string[];  readonly customScopes?: readonly string[];  readonly baseUrl?: string;  readonly specUrl?: string;  readonly authorizationUrl?: string;  readonly tokenUrl?: string;  readonly clientCredentialsTokenUrl?: string;} export interface MicrosoftGraphSpecBuild {  readonly specText: string;  readonly specUrl: string;  readonly baseUrl?: string;  readonly authorizationUrl: string;  readonly tokenUrl: string;  readonly clientCredentialsTokenUrl: string;  readonly presetIds: readonly string[];  readonly customScopes: readonly string[];  readonly scopes: readonly string[];  readonly exactPaths: readonly string[];  readonly pathPrefixes: readonly string[];  readonly tagPrefixes: readonly string[];  readonly coversFullGraph: boolean;  readonly authenticationTemplate: readonly Authentication[];} export interface MicrosoftGraphUrlPolicy {  /**   * When true, spec/base/OAuth endpoint URLs may point anywhere a trusted   * https URL could, plus plain http on loopback (local Graph emulators).   * Every other host is still rejected. Off by default — production leaves   * this unset so only the pinned Microsoft Graph URLs are accepted.   */  readonly allowUnsafeUrlOverrides?: boolean;} export type MicrosoftGraphIntegrationConfig = OpenApiIntegrationConfig & {  readonly microsoftGraphPresetIds?: readonly string[];  readonly microsoftGraphCustomScopes?: readonly string[];  readonly microsoftGraphScopes?: readonly string[];  readonly microsoftGraphExactPaths?: readonly string[];  readonly microsoftGraphPathPrefixes?: readonly string[];  readonly microsoftGraphTagPrefixes?: readonly string[];  readonly microsoftGraphCoversFullGraph?: boolean;  readonly microsoftGraphAuthorizationUrl?: string;  readonly microsoftGraphTokenUrl?: string;  readonly microsoftGraphClientCredentialsTokenUrl?: string;}; const MicrosoftGraphIntegrationConfigSchema = Schema.Struct({  specHash: Schema.optional(Schema.String),  specUrl: Schema.optional(Schema.String),  baseUrl: Schema.optional(Schema.String),  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),  queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),  authenticationTemplate: Schema.optional(Schema.Array(AuthenticationSchema)),  microsoftGraphPresetIds: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphCustomScopes: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphScopes: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphExactPaths: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphPathPrefixes: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphTagPrefixes: Schema.optional(Schema.Array(Schema.String)),  microsoftGraphCoversFullGraph: Schema.optional(Schema.Boolean),  microsoftGraphAuthorizationUrl: Schema.optional(Schema.String),  microsoftGraphTokenUrl: Schema.optional(Schema.String),  microsoftGraphClientCredentialsTokenUrl: Schema.optional(Schema.String),}); const decodeMicrosoftConfig = Schema.decodeUnknownOption(MicrosoftGraphIntegrationConfigSchema); export const decodeMicrosoftGraphIntegrationConfig = (  value: unknown,): MicrosoftGraphIntegrationConfig | null =>  Option.getOrNull(decodeMicrosoftConfig(value)) as MicrosoftGraphIntegrationConfig | null; const uniqueStrings = (values: Iterable<string>): readonly string[] => {  const seen = new Set<string>();  const result: string[] = [];  for (const value of values) {    const trimmed = value.trim();    if (!trimmed || seen.has(trimmed)) continue;    seen.add(trimmed);    result.push(trimmed);  }  return result;}; const normalizeSelection = (input: MicrosoftGraphSelectionInput) => {  const presetIds = uniqueStrings(    input.presetIds && input.presetIds.length > 0      ? input.presetIds      : MICROSOFT_GRAPH_DEFAULT_PRESET_IDS,  );  const customScopes = uniqueStrings(input.customScopes ?? []);  const scopes = microsoftGraphScopesForPresetIds(presetIds, customScopes);  const exactPaths = microsoftGraphExactPathsForPresetIds(presetIds);  const pathPrefixes = microsoftGraphPathPrefixesForPresetIds(presetIds);  const tagPrefixes = microsoftGraphTagPrefixesForPresetIds(presetIds);  const coversFullGraph = microsoftGraphPresetIdsCoverFullGraph(presetIds);  const specUrl = input.specUrl?.trim() || MICROSOFT_GRAPH_OPENAPI_URL;  const baseUrl = input.baseUrl?.trim() || undefined;  const authorizationUrl = input.authorizationUrl?.trim() || undefined;  const tokenUrl = input.tokenUrl?.trim() || undefined;  const clientCredentialsTokenUrl = input.clientCredentialsTokenUrl?.trim() || undefined;  return {    presetIds,    customScopes,    scopes,    exactPaths,    pathPrefixes,    tagPrefixes,    coversFullGraph,    specUrl,    baseUrl,    authorizationUrl,    tokenUrl,    clientCredentialsTokenUrl,  };}; interface MicrosoftOAuthEndpoints {  readonly authorizationUrl: string;  readonly tokenUrl: string;  readonly clientCredentialsTokenUrl: string;} const microsoftOAuthTemplate = (  scopes: readonly string[],  endpoints: MicrosoftOAuthEndpoints,): readonly Authentication[] => [  {    slug: AuthTemplateSlug.make(MICROSOFT_AUTH_TEMPLATE_SLUG),    kind: "oauth2",    label: MICROSOFT_DELEGATED_AUTH_LABEL,    authorizationUrl: endpoints.authorizationUrl,    tokenUrl: endpoints.tokenUrl,    scopes,  },  {    slug: AuthTemplateSlug.make(MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG),    kind: "oauth2",    label: MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL,    authorizationUrl: endpoints.authorizationUrl,    tokenUrl: endpoints.clientCredentialsTokenUrl,    scopes: [...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES],  },]; const isRecord = (value: unknown): value is Record<string, unknown> =>  value !== null && typeof value === "object" && !Array.isArray(value); const HTTP_METHODS = new Set(["delete", "get", "patch", "post", "put"]);const REQUEST_BASE_SCOPES = new Set(["offline_access", "openid", "profile", "email"]);const PATH_SCOPE_IGNORED_SCOPES = new Set([  "offline_access",  "openid",  "profile",  "email",  MICROSOFT_GRAPH_IDENTITY_SCOPE,]); const firstString = (values: readonly unknown[]): string | undefined =>  values.find((value): value is string => typeof value === "string" && value.trim().length > 0); const parseTrustedHttpsUrl = (value: string): URL | null => {  if (!URL.canParse(value)) return null;  const parsed = new URL(value);  if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) {    return null;  }  return parsed;}; // Local emulators (microsoft-emulator.test.ts, `microsoft.emulators.dev` run// locally) serve plain http on loopback. Only these three hostnames count —// this is not a general SSRF-safe "is this private" check, just a narrow// allowance for the dev machine talking to itself.const isLoopbackHostname = (hostname: string): boolean => {  const lower = hostname.toLowerCase();  return lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "[::1]";}; const parseTrustedLoopbackHttpUrl = (value: string): URL | null => {  if (!URL.canParse(value)) return null;  const parsed = new URL(value);  if (parsed.protocol !== "http:" || parsed.username || parsed.password || parsed.hash) {    return null;  }  return isLoopbackHostname(parsed.hostname) ? parsed : null;}; /** * Under `allowUnsafeUrlOverrides`, accept either a trusted https URL or a * plain-http URL on loopback (local emulators have no TLS). Every other URL * shape is still rejected, override or not. */const allowUnsafeUrl = (  value: string | undefined,  policy: MicrosoftGraphUrlPolicy | undefined,): string | undefined | null => {  if (!value) return undefined;  if (policy?.allowUnsafeUrlOverrides !== true) return null;  return parseTrustedHttpsUrl(value) || parseTrustedLoopbackHttpUrl(value) ? value : null;}; const normalizeMicrosoftGraphSpecUrl = (  value: string,  policy?: MicrosoftGraphUrlPolicy,): string | null => {  if (value === MICROSOFT_GRAPH_OPENAPI_URL) return value;  const sliceAsset = microsoftGraphSliceAssetFromUrl(value);  if (sliceAsset !== null && microsoftGraphPresetIdsForSliceAsset(sliceAsset) !== null) {    return value;  }  return allowUnsafeUrl(value, policy) ?? null;}; const MICROSOFT_GRAPH_HOSTS = new Set([  "graph.microsoft.com",  "graph.microsoft.us",  "dod-graph.microsoft.us",  "microsoftgraph.chinacloudapi.cn",]); const normalizeMicrosoftGraphBaseUrl = (  value: string | undefined,  policy?: MicrosoftGraphUrlPolicy,): string | undefined | null => {  const unsafe = allowUnsafeUrl(value, policy);  if (unsafe !== null) return unsafe;  if (!value) return undefined;  const parsed = parseTrustedHttpsUrl(value);  if (!parsed || !MICROSOFT_GRAPH_HOSTS.has(parsed.hostname.toLowerCase())) return null;  if (!/^\/(?:v1\.0|beta)(?:\/)?$/.test(parsed.pathname)) return null;  if (parsed.search) return null;  return parsed.toString().replace(/\/$/, "");}; const MICROSOFT_IDENTITY_HOSTS = new Set([  "login.microsoftonline.com",  "login.microsoftonline.us",  "login.partner.microsoftonline.cn",]); const normalizeMicrosoftOAuthEndpointUrl = (  value: string,  endpoint: "authorize" | "token",  policy?: MicrosoftGraphUrlPolicy,): string | null => {  const unsafe = allowUnsafeUrl(value, policy);  if (unsafe !== null) return unsafe ?? null;  const parsed = parseTrustedHttpsUrl(value);  if (!parsed || !MICROSOFT_IDENTITY_HOSTS.has(parsed.hostname.toLowerCase())) return null;  if (parsed.search) return null;  const suffix = endpoint === "authorize" ? "authorize" : "token";  return /^\/[^/]+\/oauth2\/v2\.0\/(?:authorize|token)$/.test(parsed.pathname) &&    parsed.pathname.endsWith(`/${suffix}`)    ? parsed.toString()    : null;}; const validateSelectionUrls = (  selection: ReturnType<typeof normalizeSelection>,  policy?: MicrosoftGraphUrlPolicy,): Effect.Effect<ReturnType<typeof normalizeSelection>, OpenApiParseError> =>  Effect.gen(function* () {    const specUrl = normalizeMicrosoftGraphSpecUrl(selection.specUrl, policy);    if (!specUrl) {      return yield* new OpenApiParseError({        message: "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI spec",      });    }    const baseUrl = normalizeMicrosoftGraphBaseUrl(selection.baseUrl, policy);    if (baseUrl === null) {      return yield* new OpenApiParseError({        message: "Microsoft Graph baseUrl must point to a supported Microsoft Graph endpoint",      });    }    const authorizationUrl = selection.authorizationUrl      ? normalizeMicrosoftOAuthEndpointUrl(selection.authorizationUrl, "authorize", policy)      : undefined;    if (selection.authorizationUrl && !authorizationUrl) {      return yield* new OpenApiParseError({        message: "Microsoft authorizationUrl must point to a supported Microsoft identity endpoint",      });    }    const tokenUrl = selection.tokenUrl      ? normalizeMicrosoftOAuthEndpointUrl(selection.tokenUrl, "token", policy)      : undefined;    if (selection.tokenUrl && !tokenUrl) {      return yield* new OpenApiParseError({        message: "Microsoft tokenUrl must point to a supported Microsoft identity endpoint",      });    }    const clientCredentialsTokenUrl = selection.clientCredentialsTokenUrl      ? normalizeMicrosoftOAuthEndpointUrl(selection.clientCredentialsTokenUrl, "token", policy)      : undefined;    if (selection.clientCredentialsTokenUrl && !clientCredentialsTokenUrl) {      return yield* new OpenApiParseError({        message:          "Microsoft clientCredentialsTokenUrl must point to a supported Microsoft identity endpoint",      });    }    return {      ...selection,      specUrl,      ...(baseUrl ? { baseUrl } : { baseUrl: undefined }),      ...(authorizationUrl ? { authorizationUrl } : { authorizationUrl: undefined }),      ...(tokenUrl ? { tokenUrl } : { tokenUrl: undefined }),      ...(clientCredentialsTokenUrl        ? { clientCredentialsTokenUrl }        : { clientCredentialsTokenUrl: undefined }),    };  }); const validateResolvedOAuthEndpoints = (  endpoints: MicrosoftOAuthEndpoints,  policy?: MicrosoftGraphUrlPolicy,): Effect.Effect<MicrosoftOAuthEndpoints, OpenApiParseError> =>  Effect.gen(function* () {    const authorizationUrl = normalizeMicrosoftOAuthEndpointUrl(      endpoints.authorizationUrl,      "authorize",      policy,    );    const tokenUrl = normalizeMicrosoftOAuthEndpointUrl(endpoints.tokenUrl, "token", policy);    const clientCredentialsTokenUrl = normalizeMicrosoftOAuthEndpointUrl(      endpoints.clientCredentialsTokenUrl,      "token",      policy,    );    if (!authorizationUrl || !tokenUrl || !clientCredentialsTokenUrl) {      return yield* new OpenApiParseError({        message: "Microsoft OAuth endpoints must point to supported Microsoft identity endpoints",      });    }    return { authorizationUrl, tokenUrl, clientCredentialsTokenUrl };  }); const recordValues = (value: unknown): readonly unknown[] =>  isRecord(value) ? Object.values(value) : []; const firstOAuthFlows = (parsed: Record<string, unknown>): readonly Record<string, unknown>[] => {  const components = isRecord(parsed.components) ? parsed.components : {};  const securitySchemes = isRecord(components.securitySchemes) ? components.securitySchemes : {};  return recordValues(securitySchemes)    .filter(isRecord)    .filter((scheme) => scheme.type === "oauth2")    .flatMap((scheme) => recordValues(scheme.flows).filter(isRecord));}; const resolveOAuthEndpoints = (  parsed: Record<string, unknown>,  overrides: {    readonly authorizationUrl?: string;    readonly tokenUrl?: string;    readonly clientCredentialsTokenUrl?: string;  },): MicrosoftOAuthEndpoints => {  const flows = firstOAuthFlows(parsed);  const authorizationCode = flows.find((flow) => flow.authorizationUrl !== undefined);  const clientCredentials = flows.find(    (flow) => flow.tokenUrl !== undefined && flow.authorizationUrl === undefined,  );  const authorizationUrl =    overrides.authorizationUrl ??    (isRecord(authorizationCode) ? firstString([authorizationCode.authorizationUrl]) : undefined) ??    MICROSOFT_AUTHORIZATION_URL;  const tokenUrl =    overrides.tokenUrl ??    (isRecord(authorizationCode) ? firstString([authorizationCode.tokenUrl]) : undefined) ??    firstString(flows.map((flow) => flow.tokenUrl)) ??    MICROSOFT_TOKEN_URL;  const clientCredentialsTokenUrl =    overrides.clientCredentialsTokenUrl ??    (isRecord(clientCredentials) ? firstString([clientCredentials.tokenUrl]) : undefined) ??    tokenUrl;  return { authorizationUrl, tokenUrl, clientCredentialsTokenUrl };}; const graphPathMatchVariants = (path: string): readonly string[] => {  const withoutVersion = path.replace(/^\/(?:v1\.0|beta)(?=\/)/, "");  return withoutVersion === path ? [path, `/v1.0${path}`] : [path, withoutVersion];}; const matchesGraphPath = (  path: string,  exactPaths: ReadonlySet<string>,  pathPrefixes: readonly string[],): boolean => {  const variants = graphPathMatchVariants(path);  if (variants.some((variant) => exactPaths.has(variant))) return true;  return variants.some((variant) =>    pathPrefixes.some(      (prefix) =>        variant === prefix || variant.startsWith(`${prefix}/`) || variant.startsWith(`${prefix}(`),    ),  );}; const isIdentityHealthPath = (path: string): boolean =>  graphPathMatchVariants(path).some((variant) => variant === "/me"); const operationTags = (operation: Record<string, unknown>): readonly string[] =>  Array.isArray(operation.tags)    ? operation.tags.filter((tag): tag is string => typeof tag === "string")    : []; const operationMatchesTagPrefix = (  operation: Record<string, unknown>,  tagPrefixes: readonly string[],): boolean =>  tagPrefixes.length > 0 &&  operationTags(operation).some((tag) =>    tagPrefixes.some((prefix) => tag === prefix || tag.startsWith(prefix)),  ); const isGraphPermissionScope = (value: string): boolean =>  value.startsWith("https://graph.microsoft.com/") ||  /^[A-Z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)+(?:\.All)?$/.test(value); export const parseMicrosoftGraphDelegatedScopes = (  permissionsReference: string,): readonly string[] =>  uniqueStrings(    permissionsReference.split(/\n(?=###\s+)/).flatMap((section) => {      const scope = section.match(/^###\s+([^\n]+)$/m)?.[1]?.trim();      if (!scope || !isGraphPermissionScope(scope)) return [];      const identifierRow = section.match(/^\|\s*Identifier\s*\|\s*([^|]*)\|\s*([^|]*)\|/m);      const delegatedIdentifier = identifierRow?.[2]?.trim();      return delegatedIdentifier && delegatedIdentifier !== "-" ? [scope] : [];    }),  ); const collectScopeStrings = (value: unknown): readonly string[] => {  if (typeof value === "string") return isGraphPermissionScope(value) ? [value] : [];  if (Array.isArray(value)) return value.flatMap(collectScopeStrings);  if (!isRecord(value)) return [];  return Object.values(value).flatMap(collectScopeStrings);}; const securityScopes = (  value: unknown,  options?: { readonly delegatedOnly?: boolean },): readonly string[] => {  if (!Array.isArray(value)) return [];  return value.flatMap((entry) => {    if (!isRecord(entry)) return [];    return Object.entries(entry).flatMap(([scheme, scopes]) => {      const lowerScheme = scheme.toLowerCase();      if (options?.delegatedOnly && lowerScheme.includes("app")) return [];      if (options?.delegatedOnly && lowerScheme.includes("application")) return [];      return Array.isArray(scopes)        ? scopes.filter((scope): scope is string => typeof scope === "string")        : [];    });

Lines 1–500 of 848.