@mochabug/adapt-plugin-toolkit
Version:
The API toolkit to facilitate mochabug adapt plugin development
4 lines • 952 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../src/router.ts", "../../src/api.ts", "../../src/genproto/mochabugapis/adapt/graph/signal_data_pb.ts", "../../src/genproto/buf/validate/validate_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/signal_format_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/jtd_schema_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/vertex_metadata_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/exchange_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/transceiver_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/signal_descriptor_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/receiver_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/signal_binding_pb.ts", "../../src/genproto/mochabugapis/adapt/graph/transmitter_pb.ts", "../../src/genproto/mochabugapis/adapt/runtime/v1/runtime_pb.ts", "../../src/genproto/mochabugapis/adapt/automations/v1/automations_pb.ts", "../../src/genproto/google/api/annotations_pb.ts", "../../src/genproto/google/api/http_pb.ts", "../../src/genproto/google/api/client_pb.ts", "../../src/genproto/google/api/launch_stage_pb.ts", "../../src/genproto/mochabugapis/adapt/runtime/v1/store_pb.ts", "../../src/grpcweb.ts", "../../src/genproto/mochabugapis/adapt/runtime/v1/incoming_pb.ts"],
"sourcesContent": ["// Copyright (c) 2023 mochabug AB. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\n// this file except in compliance with the License. You may obtain a copy of the\n// License at http://www.apache.org/licenses/LICENSE-2.0\n// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\n// WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\n// MERCHANTABLITY OR NON-INFRINGEMENT.\n// See the Apache Version 2.0 License for specific language governing permissions\n// and limitations under the License.\n\nimport { fromBinary, fromJson } from '@bufbuild/protobuf';\nimport { ConnectError } from '@connectrpc/connect';\nimport { match, MatchFunction, ParamData } from 'path-to-regexp';\nimport {\n ConfiguratorApi,\n ExecutorApi,\n mapConnectErrorToHttpStatus,\n SessionExecutorApi,\n StopExecutorApi,\n ValueJson\n} from './api';\nimport { SignalData } from './genproto/mochabugapis/adapt/graph/signal_data_pb';\nimport {\n CronTriggerRequest,\n CronTriggerRequestJson,\n CronTriggerRequestSchema,\n ExchangeResultRequest,\n ExchangeResultRequestJson,\n ExchangeResultRequestSchema,\n StartExecutionRequest,\n StartExecutionRequestJson,\n StartExecutionRequestSchema,\n StopExecutionRequest,\n StopExecutionRequestJson,\n StopExecutionRequestSchema\n} from './genproto/mochabugapis/adapt/runtime/v1/incoming_pb';\nimport {\n ConfiguratorEnvironment,\n ExecutionContext,\n ExecutorEnvironment\n} from './types';\n\nexport interface ExchangeResultWrapper extends ExchangeResultRequest {\n getSignal<T>(key: string): T;\n getSignalBinary(key: string): SignalData;\n}\n\n/**\n * Takes a sequence of path segments and joins them together into a single path.\n * This function follows Linux-style path semantics, and behaves similarly to the `path.join()` method in Node.js.\n *\n * It can handle relative path components (e.g., '..') and will correctly ascend directories when they are encountered.\n * Note that it will not resolve '..' that ascend beyond the root directory.\n * Leading and trailing slashes on individual path segments are ignored.\n * However, if the original first path starts with a slash, the final result will also start with a slash.\n *\n * @example\n * joinPaths('/foo', 'bar', 'baz/asdf', 'quux', '..'); // Returns: \"/foo/bar/baz\"\n *\n * @param {...string[]} paths - The path segments to join. Each entry should be a string representing a path segment.\n * These can include both directories and filenames, and can be absolute or relative paths.\n * Each argument can also include multiple path segments separated by slashes.\n * @returns {string} A string representing the joined path. The resulting path is normalized\n * (i.e., '..' and '.' path segments are resolved), and any redundant slashes are removed.\n * If the original first path starts with a slash, the final result will also start with a slash.\n */\nexport function joinPaths(...paths: string[]): string {\n const separator = '/';\n\n // Split each path into segments and flatten into a single array\n let segments: string[] = [];\n for (let path of paths) {\n // Split path into segments, removing empty segments caused by extra slashes\n let pathSegments = path\n .split(separator)\n .filter((segment) => segment.length > 0);\n segments.push(...pathSegments);\n }\n\n // Create an array to hold the final path segments\n let finalSegments: string[] = [];\n\n // Process each segment\n for (let segment of segments) {\n // If the segment is \"..\", remove the last segment from the final path\n if (segment === '..') {\n finalSegments.pop();\n }\n // If the segment is \".\", ignore it\n else if (segment !== '.') {\n // Otherwise, add the segment to the final path\n finalSegments.push(segment);\n }\n }\n\n // Join the final path segments\n let joinedPath = finalSegments.join(separator);\n\n // If the original path started with a \"/\", add one to the final path\n if (paths[0].startsWith(separator)) {\n joinedPath = separator + joinedPath;\n }\n\n return joinedPath;\n}\n\n/**\n * Represents a middleware function in the routing system.\n *\n * Middleware functions are designed to:\n * - Perform operations on the incoming request.\n * - Modify the execution context with additional data.\n * - Short-circuit the request handling by directly returning a response.\n * They are executed in the order they are added to the router.\n *\n * Common use cases for middleware include:\n * - Logging incoming requests.\n * - Authentication and authorization checks.\n * - Request validation.\n * - Error handling.\n * - Modifying the execution context.\n *\n * @template T\n * The type of the API that this middleware can handle. This can be:\n * - `ConfiguratorApi`: An API tailored for configuration tasks.\n * - `ExecutorApi`: An API tailored for execution tasks.\n *\n * @template C\n * The type of the execution context passed through the middleware during the request's lifecycle.\n * This context can be used to share data or state across different parts of the request processing pipeline.\n *\n * @template R\n * The return type of the middleware. By default, it can be `void` or `Response`, but can be constrained\n * to just `void` if needed.\n *\n * @param req\n * The incoming web request, providing details such as headers, body content, method, URL, etc.\n * Middleware can inspect or modify the request before it reaches the route handler.\n *\n * @param api\n * The API-specific functionalities, which could be an instance of `ConfiguratorApi` or `ExecutorApi`.\n * Middleware can utilize this API for tasks or checks related to the API's capabilities.\n *\n * @param ctx\n * The execution context for the current request. Middleware can read or modify this context to share\n * data with subsequent middleware or route handlers. For instance, an authentication middleware might\n * add a user object to the context after verifying a user's credentials.\n *\n * @param next\n * A function that passes control to the next middleware in the chain. If the current middleware is the\n * last in the chain, calling `next` will pass control to the route handler. Unless returning a response,\n * middleware should call this function to continue the request handling process.\n *\n * @returns\n * A promise that resolves to either `void` or a `Response` object. If the middleware returns `void`, the\n * next middleware or route handler is executed. If it returns a `Response`, the middleware chain is\n * short-circuited, and the response is sent to the client, bypassing subsequent middleware or route handlers.\n */\nexport type Middleware<\n T extends ConfiguratorApi | ExecutorApi,\n C extends ExecutionContext = ExecutionContext,\n R = void | Response\n> = (req: Request, api: T, ctx: C, next: () => Promise<R>) => Promise<R>;\n\n/**\n * Represents an HTTP request method.\n */\nexport type RequestMethod =\n | 'GET'\n | 'POST'\n | 'PUT'\n | 'DELETE'\n | 'PATCH'\n | 'HEAD'\n | 'OPTIONS';\nconst ALLOWED_METHODS = [\n 'GET',\n 'POST',\n 'PUT',\n 'DELETE',\n 'PATCH',\n 'HEAD',\n 'OPTIONS'\n];\n\n/**\n * Represent the method HTTP request options\n */\nexport type RequestMethodOption = RequestMethod | RequestMethod[] | '*';\n\n/**\n * Represents a set of key-value pairs for parameters.\n */\nexport type Params = Record<string, string>;\n\n/**\n * Represents the structure of the route information for a request in a server environment.\n * This is similar to the routing structure used in frameworks like Express.js.\n *\n * It provides a comprehensive view of the route which a request is being handled by,\n * encompassing the actual path, URL path parameters, and search (query) parameters.\n *\n * - `path`: Holds the route path string that the server listens on. This defines the endpoint\n * of your API and is used to match incoming requests.\n *\n * - `params`: Represents dynamic parameters within the request path. Useful for routes\n * with path parameters, e.g., `/users/:userId`, where `:userId` is a dynamic segment\n * and its actual value is captured as a parameter.\n *\n * - `searchParams`: Reflects the search (query) parameters from the request's URL.\n * Useful for filtering or altering the server's response based on these parameters.\n *\n * @property path - The path of the route to match incoming requests.\n * @property params - An object representing the path parameters.\n * @property searchParams - An object representing search (query) parameters.\n */\nexport type RouteInfo = {\n path: string;\n params: Params;\n searchParams: Params;\n};\n\n/**\n * Represents a route handler function for processing web requests.\n *\n * The handler receives the incoming request and the associated route information.\n * It's expected to return a Promise that resolves to the appropriate response.\n *\n * - `req`: Contains detailed information about the incoming request, including headers,\n * body content, method, etc.\n *\n * - `route`: Provides structured information about the matched route, including the path,\n * any dynamic path parameters, and search (query) parameters.\n *\n * @typedef RouteHandler\n * @param req - The incoming web request.\n * @param route - Information about the matched route.\n * @returns A Promise that resolves to the appropriate server response.\n */\nexport type RouteHandler = (\n req: Request,\n route: RouteInfo\n) => Promise<Response>;\n\n/**\n * Represents a route handler function tailored for APIs of type `ConfiguratorApi`\n * or `ExecutorApi`. This handler processes incoming requests by leveraging specific\n * API functionalities and returns an appropriate response.\n *\n * @template T\n * The type of the API being used in the handler. This can be either `ConfiguratorApi` or `ExecutorApi`.\n * @template C\n * The type of the execution context that provides additional information about the ongoing process or operation.\n *\n * @param req\n * The incoming web request, containing details like headers, body content, and method.\n * @param api\n * The specific API instance, representing either `ConfiguratorApi` or `ExecutorApi` functionalities.\n * @param route\n * Structured information about the matched route, including path, dynamic path parameters, and query parameters.\n * @param ctx\n * The execution context providing additional data or state for the route handler.\n *\n * @returns {Promise<Response>}\n * A Promise that resolves to the appropriate server response. Errors during processing should be caught and\n * reflected in the resulting `Response`.\n */\nexport type ApiRouteHandler<\n T extends ConfiguratorApi | ExecutorApi,\n C extends ExecutionContext = ExecutionContext\n> = (req: Request, api: T, route: RouteInfo, ctx: C) => Promise<Response>;\n\n/**\n * Represents a route handler function designed for APIs of type `StopExecutorApi`\n * or `SessionExecutorApi`. This handler processes incoming requests using specific\n * API functionalities.\n *\n * @template T\n * The type of the API being used in the handler, either `StopExecutorApi` or `SessionExecutorApi`.\n * @template C\n * The type of the execution context providing additional information about the ongoing process or operation.\n *\n * @param req\n * The incoming web request.\n * @param api\n * The specific API instance, representing functionalities of either `StopExecutorApi` or `SessionExecutorApi`.\n * @param ctx\n * The execution context providing additional data or state for the route handler.\n *\n * @returns {Promise<void>}\n * A Promise that resolves once the handler has processed the request. Errors should be caught and handled appropriately.\n */\nexport type InternalRouteHandler<\n R extends StartExecutionRequest | StopExecutionRequest | CronTriggerRequest,\n T extends StopExecutorApi | SessionExecutorApi,\n C extends ExecutionContext = ExecutionContext\n> = (req: R, api: T, ctx: C) => Promise<void>;\n\n/**\n * Represents a route handler function tailored for the `SessionExecutorApi`. This handler\n * processes incoming requests using the functionalities of `SessionExecutorApi` and responds\n * based on the specific exchange that triggered the handler.\n *\n * @template T\n * The type of the API being used in the handler, specifically `SessionExecutorApi`.\n * @template C\n * The type of the execution context providing additional information about the ongoing process or operation.\n *\n * @param req\n * The incoming web request.\n * @param api\n * The specific API instance, representing `SessionExecutorApi` functionalities.\n * @param name\n * The name of the exchange that triggered the handler.\n * @param ctx\n * The execution context providing additional data or state for the route handler.\n *\n * @returns\n * A Promise that resolves once the handler has processed the request. Errors should be caught and handled appropriately.\n */\nexport type ExchangeRouteHandler<\n R extends ExchangeResultWrapper,\n T extends SessionExecutorApi,\n C extends ExecutionContext = ExecutionContext\n> = (req: R, api: T, name: string, ctx: C) => Promise<void>;\n\n/**\n * Represents an individual route, providing functionalities to match HTTP methods\n * and URL paths against pre-defined templates.\n */\nexport class Route {\n private method: RequestMethodOption;\n private matcher: MatchFunction<ParamData>;\n\n /**\n * Constructs a new `Route` instance.\n *\n * @param {RequestMethodOption} method - The HTTP method or methods the route responds to.\n * @param {string} template - The URL path template, which may include parameters, that the route matches against.\n *\n * @throws {Error} Throws an error if the provided template is invalid.\n */\n constructor(method: RequestMethodOption, template: string) {\n this.method = method;\n this.matcher = match(template);\n }\n\n /**\n * Determines if the provided HTTP method and URL path match the route.\n *\n * If a match is found, it returns the dynamic parameters extracted from the URL.\n * If no match is found, it returns null.\n *\n * @param {RequestMethod} method - The HTTP method to check against the route's method.\n * @param {string} path - The URL path to check against the route's template.\n *\n * @return {Params | null} Returns an object with matching parameters if a match is found, otherwise null.\n */\n match(method: RequestMethod, path: string): Params | null {\n if (\n this.method !== '*' &&\n !(\n (Array.isArray(this.method) && this.method.includes(method)) ||\n this.method === method\n )\n ) {\n return null;\n }\n\n const matchResult = this.matcher(path);\n if (!matchResult) {\n return null;\n }\n const params: Params = {};\n for (const key in matchResult.params) {\n if (!matchResult.params[key]) continue;\n if (Array.isArray(matchResult.params[key])) {\n params[key] = matchResult.params[key].join('/');\n } else {\n params[key] = matchResult.params[key];\n }\n }\n return params;\n }\n}\n\ninterface RouterEntrypoint<\n T extends ConfiguratorEnvironment | ExecutorEnvironment,\n C extends ExecutionContext = ExecutionContext\n> {\n entrypoint: (req: Request, env: T, ctx: C) => Promise<Response>;\n}\n\n/**\n * `BaseRouter` serves as an abstract foundation for building routers that can process\n * web requests by matching them to predefined routes and subsequently delegating the\n * request handling to the appropriate handler.\n *\n * This base class provides core functionalities such as:\n * - Storing a collection of routes and their associated handlers.\n * - Mechanisms to add new routes.\n * - Logic to match incoming requests to the correct route.\n * - Delegating the request to the matched route's handler.\n * - Registering and executing middleware functions.\n *\n * Derived classes can extend this base class to introduce more specific behaviors or\n * to support additional types of route handlers.\n *\n * @template T\n * Represents the type of route handler that this router can handle. This can be:\n * - A basic route handler (`RouteHandler`), which processes the request and returns a response.\n * - An API-specific route handler (`ApiRouteHandler`), tailored for specific API types like\n * `ConfiguratorApi` or `ExecutorApi`.\n *\n * @template C extends ExecutionContext\n * Represents the type of the execution context that will be passed through the middleware\n * and route handlers during the request's lifecycle. This context can be used to share\n * data or state across different parts of the request processing pipeline.\n * By default, it uses the base `ExecutionContext`, but users can extend this to add\n * custom properties that are relevant to their application's logic.\n *\n * @template A\n * Represents the type of the API that the middleware and route handlers can handle. This ensures\n * that the middleware's API type matches the route handler's API type. Depending on the context,\n * this could be an instance of `ConfiguratorApi` or `ExecutorApi`.\n *\n * @example\n * // Creating a new router that handles basic route handlers and uses a custom execution context.\n * class MyRouter extends BaseRouter<RouteHandler, MyExecutionContext> { ... }\n *\n * // Creating a new router that handles API-specific route handlers for `ConfiguratorApi`.\n * class ConfiguratorRouter extends BaseRouter<ApiRouteHandler<ConfiguratorApi>, MyExecutionContext> { ... }\n */\nabstract class BaseRouter<\n T extends RouteHandler | ApiRouteHandler<any, C>,\n C extends ExecutionContext = ExecutionContext,\n A extends ConfiguratorApi | ExecutorApi = T extends ApiRouteHandler<\n infer U,\n C\n >\n ? U\n : never\n> {\n protected routes: { route: Route; handler: T }[];\n protected middlewares: Middleware<A, C, Response>[];\n\n constructor() {\n this.routes = [];\n this.middlewares = [];\n }\n\n /**\n * Registers a middleware function to log details of incoming requests.\n *\n * This middleware captures and logs essential details about the incoming request,\n * including the URL, time of the request, and certain headers that provide information\n * about the request's origin and protocol. The logged headers include `X-Forwarded-Host`,\n * `X-Forwarded-Proto`, and `X-Forwarded-For`, which are commonly used in scenarios where\n * the application is behind a proxy or load balancer.\n *\n * The log is structured for easy reading, with separators to distinguish between individual\n * log entries. This middleware is particularly useful for monitoring and debugging purposes.\n *\n * @returns\n * Returns the current router instance, allowing for method chaining.\n *\n * @example\n * const router = new MyRouter();\n *\n * // Add request logging middleware to the router\n * router.useRequestLogging();\n */\n useRequestLogging(): this {\n this.middlewares.push(async (req, _api, _ctx, next) => {\n const logTime = new Date().toISOString();\n const forwardedHost = req.headers.get('X-Forwarded-Host') || 'N/A';\n const forwardedProto = req.headers.get('X-Forwarded-Proto') || 'N/A';\n const forwardedFor = req.headers.get('X-Forwarded-For') || 'N/A';\n\n // Once the response is sent, calculate the duration and log the details.\n console.log();\n console.log(`[Request Log - ${logTime}]`);\n console.log(`----------------------------------------`);\n console.log(`URL : ${req.url}`);\n console.log(`X-Forwarded-Host : ${forwardedHost}`);\n console.log(`X-Forwarded-Proto: ${forwardedProto}`);\n console.log(`X-Forwarded-For : ${forwardedFor}`);\n console.log(`----------------------------------------`);\n console.log();\n\n return await next();\n });\n return this;\n }\n\n useErrorHandling(handler: (e: unknown) => Promise<Response>): this {\n this.middlewares.push(async (_req, _, __, next) => {\n try {\n return await next();\n } catch (e) {\n return await handler(e);\n }\n });\n return this;\n }\n\n /**\n * Registers a middleware function to handle Bearer token authorization for specific paths.\n *\n * This middleware checks the incoming request's `Authorization` header for a Bearer token.\n * If the token is present, it attempts to authorize the token using the provided API.\n * If the token is invalid or missing, the middleware returns a 401 Unauthorized response.\n *\n * The middleware can be configured to protect specific paths (those that require authorization)\n * and to exclude certain paths (those that should skip authorization).\n *\n * @param protectedPaths\n * An optional array of path prefixes that should be protected by Bearer token authorization.\n * If a request's path starts with any of the prefixes in this array, it will be subject to\n * authorization checks. If this parameter is omitted or empty, all paths will be protected by default.\n *\n * @param unprotectedPaths\n * An optional array of path prefixes that should be excluded from Bearer token authorization.\n * If a request's path starts with any of the prefixes in this array, it will skip the authorization checks.\n *\n * @returns\n * Returns the current router instance, allowing for method chaining.\n *\n * @example\n * const router = new MyRouter();\n *\n * // Protect all paths under '/api' and exclude paths under '/public'\n * router.useBearerAuthorization(['/api'], ['/public']);\n *\n * // Protect all paths under '/api' without any exclusions\n * router.useBearerAuthorization(['/api']);\n *\n * // Protect all paths by default, excluding paths under '/public'\n * router.useBearerAuthorization([], ['/public']);\n */\n useBearerAuthorization(\n protectedPaths?: string[],\n unprotectedPaths?: string[]\n ): this {\n this.middlewares.push(async (req, api, _, next) => {\n const url = new URL(req.url);\n\n // Check if the path should be excluded\n if (\n unprotectedPaths &&\n unprotectedPaths.some((ex) => url.pathname.startsWith(ex))\n ) {\n console.log(`Skipping authorization for ${url.pathname}`);\n return await next();\n }\n\n // Check if the path should be included\n if (\n protectedPaths &&\n !protectedPaths.some((inc) => url.pathname.startsWith(inc))\n ) {\n console.log(`Skipping authorization for ${url.pathname}`);\n return await next();\n }\n\n const authHeader = req.headers.get('Authorization')!;\n if (authHeader && authHeader.startsWith('Bearer ')) {\n const token = authHeader.split(' ')[1];\n if (!token) {\n console.log('No token found in header');\n return new Response(null, { status: 401 });\n }\n try {\n await api.authorize(token);\n } catch (e) {\n console.log(e);\n console.error(e);\n if (e instanceof ConnectError) {\n return new Response(null, {\n status: mapConnectErrorToHttpStatus(e)\n });\n }\n return new Response(null, { status: 500 });\n }\n console.log('Authorization successful');\n return await next();\n } else {\n console.log('No Authorization header found');\n return new Response('Unauthorized', { status: 401 });\n }\n });\n return this;\n }\n\n /**\n * Registers a middleware function to be executed before reaching the route handlers.\n *\n * Middleware functions are designed to perform operations on the incoming request,\n * potentially modify the execution context, or even short-circuit the request handling\n * by directly returning a response. They are executed in the order they are added to the router.\n *\n * This method allows for the chaining of middleware additions, enabling a fluent API style.\n *\n * @param middleware\n * The middleware function to be added. This function will have access to the request,\n * the execution context, and a `next` function. It can either modify the request or context\n * and call `next` to continue the middleware chain, or return a `Response` to short-circuit\n * the request handling.\n *\n * @returns\n * Returns the current router instance, allowing for method chaining.\n *\n * @example\n * // Creating a new router instance\n * const router = new MyRouter();\n *\n * // Using middleware for Bearer token authentication\n * router.use(async (req, ctx, next) => {\n * const authHeader = req.headers['authorization'];\n * if (authHeader && authHeader.startsWith('Bearer ')) {\n * const token = authHeader.split(' ')[1];\n * // Validate the token and set user in context (pseudo-code)\n * ctx.user = await validateToken(token);\n * await next();\n * } else {\n * return new Response('Unauthorized', { status: 401 });\n * }\n * });\n *\n * // Using middleware to check for a specific query parameter\n * router.use((req, ctx, next) => {\n * if (req.searchParams.has('myParam')) {\n * await next();\n * } else {\n * return new Response('Bad Request', { status: 400 });\n * }\n * });\n *\n * // Using middleware to check for a specific cookie\n * router.use((req, ctx, next) => {\n * const cookies = parseCookies(req.headers.cookie); // Assuming a function to parse cookies\n * if (cookies['myCookie']) {\n * ctx.myCookieValue = cookies['myCookie'];\n * await next();\n * } else {\n * return new Response('Cookie not found', { status: 400 });\n * }\n * });\n */\n use(middleware: Middleware<A, C, Response>): this {\n this.middlewares.push(middleware);\n return this;\n }\n\n /**\n * Adds a new route to the router.\n *\n * @param method - The HTTP method(s) for the route.\n * @param template - The URL template for the route.\n * @param handler - The handler that should be invoked when the route is matched.\n * @returns - Returns the router instance for chaining.\n */\n add(method: RequestMethodOption, template: string, handler: T): this {\n const route = new Route(method, template);\n this.routes.push({ route, handler });\n return this;\n }\n\n /**\n * Matches an incoming request to a route and invokes the appropriate handler.\n *\n * @param req - The incoming request.\n * @param handlerCallback - The callback function that\n * handles the request once a matching route is found.\n * @returns - Returns the response from the handler or appropriate error response.\n */\n protected async routeRequest(\n req: Request,\n handlerCallback: (handler: T, routeInfo: RouteInfo) => Promise<Response>\n ): Promise<Response> {\n const urlObj = new URL(req.url);\n const path = urlObj.pathname;\n\n const searchParams: Params = {};\n for (const [key, value] of urlObj.searchParams) {\n searchParams[key] = value;\n }\n\n for (let { route, handler } of this.routes) {\n const params = route.match(req.method as RequestMethod, path);\n if (params !== null) {\n return await handlerCallback(handler, { path, params, searchParams });\n }\n }\n return new Response('Not found', { status: 404 });\n }\n}\n\n// Note: The following routers follow a similar pattern but are tailored for specific API types.\n\n/**\n * Router specialized in handling routes for the `ExecutorApi`.\n */\nexport class ExternalExecutorRouter<\n C extends ExecutionContext = ExecutionContext\n >\n extends BaseRouter<ApiRouteHandler<ExecutorApi, C>, C>\n implements RouterEntrypoint<ExecutorEnvironment, C>\n{\n constructor() {\n super();\n }\n\n public async entrypoint(\n req: Request,\n env: ExecutorEnvironment,\n ctx: C\n ): Promise<Response> {\n // Check if the request method is allowed\n if (!ALLOWED_METHODS.includes(req.method.toUpperCase())) {\n return new Response('Method Not Allowed', { status: 405 });\n }\n\n const pluginToken = req.headers.get('X-Mochabug-Adapt-Plugin-Token');\n if (!pluginToken) {\n return new Response('Unauthorized', { status: 401 });\n }\n\n try {\n // Middleware execution\n const api = new ExecutorApi(env, pluginToken);\n let middlewareIndex = 0;\n const executeMiddleware = async (): Promise<Response> => {\n if (middlewareIndex < this.middlewares.length) {\n const middleware = this.middlewares[middlewareIndex];\n middlewareIndex++;\n return await middleware(req, api, ctx, executeMiddleware);\n } else {\n // If no more middlewares, execute the route handler as the final step\n return await this.routeRequest(req, async (handler, routeParams) => {\n return handler(req, api, routeParams, ctx);\n });\n }\n };\n\n return await executeMiddleware();\n } catch (e) {\n console.log('An error was catched during middleware execution');\n console.log(e);\n console.error(e);\n return new Response('Internal Server Error', { status: 500 });\n }\n }\n}\n\n/**\n * `InternalExecutorRouter` is a specialized router designed to handle routes specifically for the `SessionExecutorApi`.\n *\n * This router provides functionalities to:\n * - Register routes with associated handlers.\n * - Register exchange routes with associated handlers.\n * - Add middleware functions that can process and potentially modify incoming requests before they reach the route handlers.\n *\n * @template C\n * Represents the type of the execution context that will be passed through the middleware and route handlers during the request's lifecycle.\n * This context can be used to share data or state across different parts of the request processing pipeline.\n * By default, it uses the base `ExecutionContext`, but users can extend this to add custom properties that are relevant to their application's logic.\n */\nexport class InternalExecutorRouter<\n C extends ExecutionContext = ExecutionContext\n> implements RouterEntrypoint<ExecutorEnvironment, C>\n{\n private startRoute?: {\n route: Route;\n handler: InternalRouteHandler<StartExecutionRequest, any, C>;\n };\n\n private stopRoute?: {\n route: Route;\n handler: InternalRouteHandler<StopExecutionRequest, any, C>;\n };\n\n private exchangeRoutes: {\n route: Route;\n handler: ExchangeRouteHandler<ExchangeResultWrapper, SessionExecutorApi, C>;\n }[];\n\n // Used in the cron subclass\n protected cronRoute?: {\n route: Route;\n handler: InternalRouteHandler<CronTriggerRequest, any, C>;\n };\n\n /**\n * Creates a new instance of InternalExecutorRouter.\n */\n constructor() {\n this.exchangeRoutes = [];\n }\n\n public async entrypoint(\n req: Request,\n env: ExecutorEnvironment,\n ctx: C\n ): Promise<Response> {\n // Check if the request method is allowed\n if (!ALLOWED_METHODS.includes(req.method.toUpperCase())) {\n return new Response('Method Not Allowed', { status: 405 });\n }\n\n const pluginToken = req.headers.get('X-Mochabug-Adapt-Plugin-Token');\n if (!pluginToken) {\n return new Response('Unauthorized', { status: 401 });\n }\n\n try {\n const authHeader = req.headers.get('Authorization');\n const api = new SessionExecutorApi(\n env,\n pluginToken,\n authHeader ? authHeader : ''\n );\n\n const urlObj = new URL(req.url);\n const path = urlObj.pathname.endsWith('/')\n ? urlObj.pathname.slice(0, -1)\n : urlObj.pathname;\n\n let useJson = false;\n switch (req.headers.get('Content-Type')) {\n case 'application/json':\n useJson = true;\n break;\n case 'application/protobuf':\n useJson = false;\n break;\n default:\n return new Response('Unsupported Media Type', { status: 415 });\n }\n\n // Try the start,stop,cron routes\n if (this.startRoute?.route.match(req.method as RequestMethod, path)) {\n let startReq: StartExecutionRequest;\n if (useJson) {\n startReq = fromJson(\n StartExecutionRequestSchema,\n (await req.json()) as StartExecutionRequestJson\n );\n } else {\n startReq = fromBinary(\n StartExecutionRequestSchema,\n new Uint8Array(await req.arrayBuffer())\n );\n }\n await this.startRoute.handler(startReq, api, ctx);\n }\n if (this.stopRoute?.route.match(req.method as RequestMethod, path)) {\n let stopReq: StopExecutionRequest;\n if (useJson) {\n stopReq = fromJson(\n StopExecutionRequestSchema,\n (await req.json()) as StopExecutionRequestJson\n );\n } else {\n stopReq = fromBinary(\n StopExecutionRequestSchema,\n new Uint8Array(await req.arrayBuffer())\n );\n }\n await this.stopRoute.handler(stopReq, api, ctx);\n }\n if (this.cronRoute?.route.match(req.method as RequestMethod, path)) {\n let cronReq: CronTriggerRequest;\n if (useJson) {\n cronReq = fromJson(\n CronTriggerRequestSchema,\n (await req.json()) as CronTriggerRequestJson\n );\n } else {\n cronReq = fromBinary(\n CronTriggerRequestSchema,\n new Uint8Array(await req.arrayBuffer())\n );\n }\n await this.cronRoute.handler(cronReq, api, ctx);\n }\n\n // Try the stream routes\n for (let { route, handler } of this.exchangeRoutes) {\n const params = route.match(req.method as RequestMethod, path);\n if (params?.name) {\n let exchangeReq: ExchangeResultRequest;\n if (useJson) {\n exchangeReq = fromJson(\n ExchangeResultRequestSchema,\n (await req.json()) as ExchangeResultRequestJson\n );\n } else {\n exchangeReq = fromBinary(\n ExchangeResultRequestSchema,\n new Uint8Array(await req.arrayBuffer())\n );\n }\n await handler(\n {\n ...exchangeReq,\n getSignal: <T = ValueJson | undefined>(key: string) =>\n JSON.parse(\n new TextDecoder().decode(\n exchangeReq.result!.signals[key].data\n )\n ) as T,\n getSignalBinary: (key: string) => exchangeReq.result!.signals[key]\n },\n api,\n params.name,\n ctx\n );\n break;\n }\n }\n\n return new Response();\n } catch (e) {\n console.log('An error was catched during middleware execution');\n console.log(e);\n console.error(e);\n return new Response('Internal Server Error', { status: 500 });\n }\n }\n\n /**\n * Registers a start handler for the vertex start endpoint\n *\n * @public\n * @param handle - The route handler for the start event.\n * @returns - Returns the instance of the router for method chaining.\n */\n public onStart(\n handle: InternalRouteHandler<StartExecutionRequest, SessionExecutorApi, C>\n ): this {\n this.startRoute = { route: new Route('POST', '/start'), handler: handle };\n return this;\n }\n\n /**\n * Registers a stop handler in case the vertex execution is stopped. Typically used for triggers and\n * vertices that requires cleanup\n *\n * @public\n * @param handle - The route handler for the stop event.\n * @returns - Returns the instance of the router for method chaining.\n */\n public onStop(\n handle: InternalRouteHandler<StopExecutionRequest, StopExecutorApi, C>\n ): this {\n this.stopRoute = { route: new Route('POST', '/stop'), handler: handle };\n return this;\n }\n\n /**\n * Registers a stream handler for a stream callback.\n *\n * @public\n * @param handle - The route handler for the stream event.\n * @returns - Returns the instance of the router for method chaining.\n */\n public onExchange(\n handle: ExchangeRouteHandler<ExchangeResultWrapper, SessionExecutorApi, C>\n ): this {\n this.exchangeRoutes.push({\n route: new Route('POST', `/exchanges/:name`),\n handler: handle\n });\n return this;\n }\n}\n\nexport class CronExecutorRouter<\n C extends ExecutionContext = ExecutionContext\n> extends InternalExecutorRouter<C> {\n constructor() {\n super();\n }\n\n /**\n * Registers a cron handler for the cron events.\n *\n * @public\n * @param handle - The route handler for the stop event.\n * @returns - Returns the instance of the router for method chaining.\n */\n public onCron(\n handle: InternalRouteHandler<CronTriggerRequest, SessionExecutorApi, C>\n ): this {\n this.cronRoute = { route: new Route('POST', '/cron'), handler: handle };\n return this;\n }\n}\n\n/**\n * Router specialized in handling routes for the `ConfiguratorApi`.\n */\nexport class ExternalConfiguratorRouter<\n C extends ExecutionContext = ExecutionContext\n >\n extends BaseRouter<ApiRouteHandler<ConfiguratorApi, C>, C>\n implements RouterEntrypoint<ConfiguratorEnvironment, C>\n{\n constructor() {\n super();\n }\n\n public async entrypoint(\n req: Request,\n env: ConfiguratorEnvironment,\n ctx: C\n ): Promise<Response> {\n if (!ALLOWED_METHODS.includes(req.method.toUpperCase())) {\n return new Response('Method Not Allowed', { status: 405 });\n }\n\n const pluginToken = req.headers.get('X-Mochabug-Adapt-Plugin-Token');\n if (!pluginToken) {\n return new Response('Unauthorized', { status: 401 });\n }\n\n const authHeader = req.headers.get('Authorization');\n const authToken =\n authHeader && authHeader.startsWith('Bearer ')\n ? authHeader.substring(7)\n : pluginToken;\n\n try {\n // Middleware execution\n let middlewareIndex = 0;\n const api = new ConfiguratorApi(env, authToken);\n const executeMiddleware = async (): Promise<Response> => {\n if (middlewareIndex < this.middlewares.length) {\n const middleware = this.middlewares[middlewareIndex];\n middlewareIndex++;\n return await middleware(req, api, ctx, executeMiddleware);\n } else {\n return await this.routeRequest(req, async (handler, routeParams) => {\n return handler(req, api, routeParams, ctx);\n });\n }\n };\n\n return await executeMiddleware();\n } catch (e) {\n console.log('An error was catched during middleware execution');\n console.log(e);\n console.error(e);\n return new Response('Internal Server Error', { status: 500 });\n }\n }\n}\n\n/**\n * Router specialized in handling routes for the `ConfiguratorApi`.\n */\nexport class InternalConfiguratorRouter\n implements RouterEntrypoint<ConfiguratorEnvironment, ExecutionContext>\n{\n constructor() {}\n\n public async entrypoint(\n _req: Request,\n _env: ConfiguratorEnvironment,\n _ctx: ExecutionContext\n ): Promise<Response> {\n return new Response('Not implemented', { status: 501 });\n }\n}\n", "// Copyright (c) 2023 mochabug AB. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\n// this file except in compliance with the License. You may obtain a copy of the\n// License at http://www.apache.org/licenses/LICENSE-2.0\n// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\n// WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\n// MERCHANTABLITY OR NON-INFRINGEMENT.\n// See the Apache Version 2.0 License for specific language governing permissions\n// and limitations under the License.\n\nimport {\n create,\n enumFromJson,\n fromJson,\n JsonValue,\n toJson\n} from '@bufbuild/protobuf';\nimport {\n FieldMaskSchema,\n timestampDate,\n timestampFromDate,\n ValueJson,\n ValueSchema\n} from '@bufbuild/protobuf/wkt';\nimport { Client, Code, ConnectError, createClient } from '@connectrpc/connect';\nimport {\n SignalData,\n SignalDataSchema\n} from './genproto/mochabugapis/adapt/graph/signal_data_pb';\nimport {\n SignalFormatJson,\n SignalFormatSchema\n} from './genproto/mochabugapis/adapt/graph/signal_format_pb';\nimport {\n VertexMetadataJson,\n VertexMetadataSchema\n} from './genproto/mochabugapis/adapt/graph/vertex_metadata_pb';\nimport {\n ConfiguratorService,\n ExchangeOperation,\n ExecutorService,\n ListIncomingSignalsResponseJson,\n ListIncomingSignalsResponseSchema,\n Namespace,\n NamespaceJson,\n NamespaceSchema,\n PluginService,\n Token\n} from './genproto/mochabugapis/adapt/runtime/v1/runtime_pb';\nimport {\n ConditionalDeleteOp_Precondition,\n ConditionalDeleteOp_PreconditionSchema,\n ConditionalInsertOp_Precondition,\n ConditionalInsertOp_PreconditionSchema,\n GetValue,\n SelectOpJson,\n TimestampRangeSchema,\n WriteOperation,\n WriteOperationSchema\n} from './genproto/mochabugapis/adapt/runtime/v1/store_pb';\nimport { createGrpcWebTransport } from './grpcweb';\nimport {\n ConfiguratorEnvironment,\n Environment,\n ExecutorEnvironment\n} from './types';\n\nexport * from './genproto/mochabugapis/adapt/automations/v1/automations_pb';\nexport * from './genproto/mochabugapis/adapt/graph/exchange_pb';\nexport * from './genproto/mochabugapis/adapt/graph/jtd_schema_pb';\nexport * from './genproto/mochabugapis/adapt/graph/receiver_pb';\nexport * from './genproto/mochabugapis/adapt/graph/signal_binding_pb';\nexport * from './genproto/mochabugapis/adapt/graph/signal_data_pb';\nexport * from './genproto/mochabugapis/adapt/graph/signal_descriptor_pb';\nexport * from './genproto/mochabugapis/adapt/graph/signal_format_pb';\nexport * from './genproto/mochabugapis/adapt/graph/transceiver_pb';\nexport * from './genproto/mochabugapis/adapt/graph/transmitter_pb';\nexport * from './genproto/mochabugapis/adapt/graph/vertex_metadata_pb';\nexport * from './genproto/mochabugapis/adapt/runtime/v1/incoming_pb';\nexport * from './genproto/mochabugapis/adapt/runtime/v1/runtime_pb';\nexport * from './genproto/mochabugapis/adapt/runtime/v1/store_pb';\nexport * from './types';\nexport { Code, ConnectError };\nexport type { ValueJson };\n\n/**\n * Convert camelCase field names to snake_case for protocol buffer field masks\n */\nconst CAMEL_TO_SNAKE_MAP: Record<string, string> = {\n config: 'config',\n metadata: 'metadata',\n configuredServices: 'configured_services'\n};\n\nfunction toSnakeCase(camelCase: string): string {\n return CAMEL_TO_SNAKE_MAP[camelCase] ?? camelCase;\n}\n\n/**\n * Array representing asset directories.\n */\nexport type AssetDirectory = {\n /**\n * Name of the asset directory.\n */\n name: string;\n\n /**\n * Type of the asset (file or directory).\n */\n type: 'file' | 'directory';\n}[];\n\n/**\n * Represents the content of an asset file.\n */\nexport type AssetFile = {\n /**\n * Content of the asset file as a readable stream.\n */\n content: ReadableStream;\n\n /**\n * MIME type of the asset file.\n */\n mime: string;\n};\n\n/**\n * Base class for API interactions. Extended by specific API implementations.\n * Not intended for direct use.\n */\nexport class ApiBase {\n protected env: Environment;\n protected pluginService: Client<typeof PluginService>;\n protected pluginToken: string;\n\n /**\n * Initializes the ApiBase instance with environment settings and a plugin token.\n *\n * @param env - Environment configuration for API communication.\n * @param pluginToken - Token for plugin authentication.\n */\n constructor(env: Environment, pluginToken: string) {\n this.env = env;\n this.pluginToken = pluginToken;\n this.pluginService = createClient(\n PluginService,\n createGrpcWebTransport({\n fetcher: this.env.plugin,\n interceptors: [\n (next) => async (req) => {\n req.header.set('Authorization', `Bearer ${this.pluginToken}`);\n return await next(req);\n }\n ]\n })\n );\n }\n\n /**\n * Retrieves a single plugin-scoped variable by its name.\n * Supports dot notation for nested variable names (e.g., 'api.credentials.key').\n *\n * @param name - The name of the variable (1-100 chars, pattern: ^[_$a-zA-Z][_$a-zA-Z0-9]*(?:\\.[_$a-zA-Z][_$a-zA-Z0-9]*)*$).\n * @returns A promise that resolves with the variable value (undefined if not found).\n *\n * @example\n * ```typescript\n * const apiUrl = await api.getSystemVariable<string>('api.url');\n * const nested = await api.getSystemVariable<string>('config.database.host');\n * ```\n */\n async getSystemVariable<T = ValueJson>(name: string): Promise<T> {\n const response = await this.pluginService.batchGetSystemVariables({\n names: [name]\n });\n const vares = response.items[name];\n return (vares ? toJson(ValueSchema, vares) : undefined) as T;\n }\n\n /**\n * Retrieves multiple plugin-scoped variables by their names with full type safety.\n * Supports dot notation for nested variable names.\n *\n * @template T - Record type mapping variable names to their expected types.\n * @param keys - Names of the variables to retrieve (1-100 unique names, each 1-100 chars).\n * @returns A promise that resolves with an object mapping variable names to their typed values.\n * @throws {ConnectError} Code.InvalidArgument if keys array is empty or exceeds 100 items.\n *\n * @example\n * ```typescript\n * const vars = await api.getSystemVariables<{\n * 'api.url': string;\n * 'api.timeout': number;\n * 'api.enabled': boolean;\n * }>('api.url', 'api.timeout', 'api.enabled');\n *\n * // Fully typed!\n * vars['api.url']; // string\n * vars['api.timeout']; // number\n * vars['api.enabled']; // boolean\n * ```\n */\n async getSystemVariables<T extends Record<string, any>>(\n ...keys: Array<keyof T & string>\n ): Promise<T> {\n const response = await this.pluginService.batchGetSystemVariables({\n names: keys\n });\n const res: Record<string, ValueJson> = {};\n for (const [key, value] of Object.entries(response.items)) {\n res[key] = toJson(ValueSchema, value);\n }\n return res as T;\n }\n\n /**\n * Retrieves a single user-scoped variable by its name.\n * Supports dot notation for nested variable names (e.g., 'user.preferences.theme').\n *\n * @param name - The name of the variable (1-100 chars, pattern: ^[_$a-zA-Z][_$a-zA-Z0-9]*(?:\\.[_$a-zA-Z][_$a-zA-Z0-9]*)*$).\n * @returns A promise that resolves with the user variable value (undefined if not found).\n *\n * @example\n * ```typescript\n * const theme = await api.getUserVariable<string>('user.preferences.theme');\n * const email = await api.getUserVariable<string>('user.email');\n * ```\n */\n async getUserVariable<T = ValueJson>(name: string): Promise<T> {\n const response = await this.pluginService.batchGetUserVariables({\n names: [name]\n });\n const vares = response.items[name];\n return (vares ? toJson(ValueSchema, vares) : undefined) as T;\n }\n\n /**\n * Retrieves multiple user-scoped variables by their names with full type safety.\n * Supports dot notation for nested variable names.\n *\n * @template T - Record type mapping variable names to their expected types.\n * @param keys - Names of the user variables to retrieve (1-100 unique names, each 1-100 chars).\n * @returns A promise that resolves with an object mapping variable names to their typed values.\n * @throws {ConnectError} Code.InvalidArgument if keys array is empty or exceeds 100 items.\n *\n * @example\n * ```typescript\n * const vars = await api.getUserVariables<{\n * 'user.email': string;\n * 'user.age': number;\n * 'user.verified': boolean;\n * }>('user.email', 'user.age'