{"version":3,"file":"annotations.mjs","names":["url: URL"],"sources":["../src/annotations/annotations.ts"],"sourcesContent":["import OpenAPIBackend from \"openapi-backend\"\nimport { OpenAPIV3 } from \"openapi-types\"\nimport { Annotation, Target } from \"../main/types\"\n// We import via alias to avoid double bundling of sdk functions\nimport {\n  Permission,\n  PermissionCoerced,\n  permissionId,\n  validatePresets,\n  // eslint does not know about our Typescript path alias\n  // eslint-disable-next-line import/no-unresolved\n} from \"zodiac-roles-sdk\"\n\ntype DeferencedOpenAPIParameter = Omit<OpenAPIV3.ParameterObject, \"schema\"> & {\n  schema: OpenAPIV3.SchemaObject\n}\n\nexport interface Preset {\n  permissions: PermissionCoerced[]\n  uri: string\n  serverUrl: string\n  apiInfo: OpenAPIV3.InfoObject\n  path: string\n  params: Record<string, string | number>\n  query: Record<string, string | number | string[] | number[]>\n  operation: {\n    summary?: string\n    description?: string\n    tags?: string[]\n    parameters: DeferencedOpenAPIParameter[]\n  }\n}\n\n/**\n * Processes annotations and validates against a list of permissions (intended\n * to be the on-chain permissions).\n *\n * This function:\n * 1. Resolves each annotation (uri and schema) into a permission preset\n * 2. Validates each preset against the input list (must be strict subset)\n * 3. Discards invalid presets\n * 4. Returns validated presets plus remaining permissions not covered by presets\n *\n * @param {readonly Permission[]} permissions - List of permissions to validate against\n * @param {readonly Annotation[]} annotations - Annotations to resolve and validate\n * @param {Object} [options] - Optional functions for fetching resources\n * @param {Function} [options.fetchPermissions] - Fetch permissions from URL\n * @param {Function} [options.fetchSchema] - Fetch schema from URL\n *\n * @returns {Promise<{ presets: Preset[], permissions: Permission[] }>}\n *   - `presets`: Valid presets (strict subsets of input permissions)\n *   - `permissions`: Input permissions not covered by any preset\n */\nexport const processAnnotations = async (\n  targets: readonly Target[],\n  annotations: readonly Annotation[],\n  options: {\n    fetchPermissions?: (url: string) => Promise<Permission[]>\n    fetchSchema?: (url: string) => Promise<OpenAPIV3.Document>\n  } = {}\n) => {\n  const {\n    fetchPermissions = defaultJsonFetch as (\n      url: string\n    ) => Promise<Permission[]>,\n    fetchSchema = defaultJsonFetch as (\n      url: string\n    ) => Promise<OpenAPIV3.Document>,\n  } = options\n\n  const cache = new Map<string, Promise<OpenAPIBackend>>()\n  const cachedFetchSchema = (url: string) => {\n    const fetchSchemaAndInitApi = async () => {\n      const schema = await fetchSchema(url)\n\n      // Resilient access to CJS-only openapi-backend to not force a special CJS<>ESM interop config on users\n      const ResilientOpenAPIBackend =\n        \"default\" in OpenAPIBackend\n          ? (OpenAPIBackend.default as typeof OpenAPIBackend)\n          : OpenAPIBackend\n\n      const api = new ResilientOpenAPIBackend({\n        definition: schema,\n        quick: true, // makes $ref resolution synchronous\n      })\n      await api.init()\n      return api\n    }\n\n    if (!cache.has(url)) {\n      cache.set(url, fetchSchemaAndInitApi())\n    }\n\n    return cache.get(url)!\n  }\n\n  const presets = await Promise.all(\n    annotations.map(async (annotation) => {\n      try {\n        return await resolveAnnotation(annotation, {\n          fetchPermissions,\n          fetchSchema: cachedFetchSchema,\n        })\n      } catch (e) {\n        console.error(\"Error resolving annotation\", e)\n        return null\n      }\n    })\n  )\n\n  return validatePresets({ targets, presets })\n}\n\nexport const resolveAnnotation = async (\n  annotation: Annotation,\n  {\n    fetchPermissions,\n    fetchSchema,\n  }: {\n    fetchPermissions: (url: string) => Promise<Permission[]>\n    fetchSchema: (url: string) => Promise<OpenAPIBackend>\n  }\n): Promise<Preset | null> => {\n  const normalizedUri = normalizeUri(annotation.uri)\n\n  const [permissions, schema] = await Promise.all([\n    fetchPermissions(normalizedUri).catch((e: Error) => {\n      console.error(`Error resolving annotation ${normalizedUri}`, e)\n      throw new Error(`Error resolving annotation: ${e}`)\n    }),\n    fetchSchema(annotation.schema).catch((e) => {\n      console.error(`Error resolving annotation schema ${annotation.schema}`, e)\n      throw new Error(`Error resolving annotation schema: ${e}`)\n    }),\n  ] as const)\n\n  if (!permissions) {\n    throw new Error(\"invalid fetchPermissions result\")\n  }\n  if (permissions.length === 0) {\n    throw new Error(`Annotation resolves to empty permission set`)\n  }\n\n  const { serverUrl, path, query } = parseUri(\n    normalizedUri,\n    schema,\n    annotation.schema\n  )\n  const {\n    operation,\n    params,\n    query: parsedQuery,\n  } = matchAction(schema, { path, query })\n\n  if (!operation) {\n    console.error(\"No operation found for annotation\", annotation)\n    return null\n  }\n\n  // The schema is already fully dereferenced, but the types don't reflect that\n  // Thus the manual type cast.\n  const operationParameters =\n    operation.parameters as DeferencedOpenAPIParameter[]\n\n  return {\n    permissions: dedupePermissions(permissions),\n    uri: normalizedUri,\n    serverUrl,\n    apiInfo: schema.definition.info || { title: \"\", version: \"\" },\n    path: operation.path,\n    params: params,\n    query: parsedQuery,\n    operation: {\n      summary: operation.summary,\n      tags: operation.tags,\n      parameters: operationParameters,\n    },\n  }\n}\n\n/** Normalize a URI to a canonical form in which query params are sorted alphabetically */\nconst normalizeUri = (uri: string) => {\n  let url: URL\n  try {\n    url = new URL(uri) // Parse the URI into its components\n  } catch (error) {\n    throw new Error(`Invalid URI: ${uri}`) // Handle invalid URI errors\n  }\n\n  const searchParams = new URLSearchParams(url.search)\n  // Sort the key/value pairs in place\n  searchParams.sort()\n\n  // Reconstruct the URI with normalized query parameters\n  return `${url.origin}${url.pathname}?${searchParams}`\n}\n\n/** Returns the annotation's path relative to the API server's base URL */\nconst parseUri = (\n  annotationUrl: string,\n  schema: OpenAPIBackend,\n  schemaUrl: string\n) => {\n  // Server urls may be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served.\n  // We resolve them to absolute urls.\n  const serverUrls =\n    !schema.document.servers || schema.document.servers.length === 0\n      ? [\"/\"]\n      : schema.document.servers.map((server) => server.url)\n  const absoluteServerUrls = serverUrls.map(\n    (serverUrl) => new URL(serverUrl, schemaUrl).href\n  )\n\n  const matchingServerUrl = absoluteServerUrls.find((serverUrl) =>\n    new URL(annotationUrl, serverUrl).href.startsWith(serverUrl)\n  )\n\n  if (!matchingServerUrl) {\n    throw new Error(\n      `Annotation url ${annotationUrl} is not within any server url declared in the schema ${schemaUrl}`\n    )\n  }\n\n  // extract the pathname after the server base path and the query string\n  const pathAndQuery = new URL(annotationUrl, matchingServerUrl).href.slice(\n    matchingServerUrl.length\n  )\n  const { pathname, search } = new URL(pathAndQuery, \"http://0/\")\n\n  return {\n    serverUrl: matchingServerUrl,\n    path: pathname,\n    query: search,\n  }\n}\n\n/** Matches a request against the operations in the OpenAPI schema  */\nconst matchAction = (\n  schema: OpenAPIBackend,\n  { path, query }: { path: string; query: string }\n) => {\n  let request = schema.router.parseRequest({\n    method: \"GET\",\n    path,\n    query,\n    headers: {},\n  })\n  // find matching operation from schema\n  const operation = schema.router.matchOperation(request)\n\n  if (operation) {\n    // parse request again, now with matched operation so that request.query and request.params will correctly parsed\n    request = schema.router.parseRequest(request, operation)\n  }\n\n  return {\n    operation,\n    params: request.params,\n    query: request.query,\n  }\n}\n\nconst defaultJsonFetch = async (url: string) => {\n  const res = await fetch(url)\n  return await validateJsonResponse(res)\n}\n\nasync function validateJsonResponse(res: Response) {\n  if (res.ok) {\n    return await res.json()\n  }\n\n  // try to parse the response as json to get the error message\n  let json = null\n  try {\n    json = await res.json()\n  } catch (e) {\n    throw new Error(\"Could not parse as json\")\n  }\n  if (json.error) {\n    throw new Error(json.error)\n  }\n\n  throw new Error(`Request failed: ${res.statusText}`)\n}\n\nfunction dedupePermissions(\n  permissions: PermissionCoerced[]\n): PermissionCoerced[] {\n  const seen = new Set<string>()\n  return permissions.filter((p) => {\n    const id = permissionId(p)\n    return seen.has(id) ? false : seen.add(id) && true\n  })\n}\n"],"mappings":"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"}