UNPKG

@ubiquity-os/plugin-sdk

Version:

SDK for plugin support.

562 lines (544 loc) 23 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { CommentHandler: () => CommentHandler, createActionsPlugin: () => createActionsPlugin, createPlugin: () => createPlugin }); module.exports = __toCommonJS(src_exports); // src/server.ts var import_value2 = require("@sinclair/typebox/value"); var import_ubiquity_os_logger3 = require("@ubiquity-os/ubiquity-os-logger"); var import_hono = require("hono"); var import_adapter2 = require("hono/adapter"); var import_http_exception = require("hono/http-exception"); // src/helpers/runtime-info.ts var import_github = __toESM(require("@actions/github")); var import_adapter = require("hono/adapter"); var PluginRuntimeInfo = class _PluginRuntimeInfo { static _instance = null; _env = {}; constructor(env) { if (env) { this._env = env; } } static getInstance(env) { if (!_PluginRuntimeInfo._instance) { _PluginRuntimeInfo._instance = (0, import_adapter.getRuntimeKey)() === "workerd" ? new CfRuntimeInfo(env) : new NodeRuntimeInfo(env); } return _PluginRuntimeInfo._instance; } }; var CfRuntimeInfo = class extends PluginRuntimeInfo { get version() { return Promise.resolve(this._env.CLOUDFLARE_VERSION_METADATA?.id ?? "CLOUDFLARE_VERSION_METADATA"); } get runUrl() { const accountId = this._env.CLOUDFLARE_ACCOUNT_ID ?? "<missing-cloudflare-account-id>"; const workerName = this._env.CLOUDFLARE_WORKER_NAME; const toTime = Date.now() + 6e4; const fromTime = Date.now() - 6e4; const timeParam = encodeURIComponent(`{"type":"absolute","to":${toTime},"from":${fromTime}}`); return `https://dash.cloudflare.com/${accountId}/workers/services/view/${workerName}/production/observability/logs?granularity=0&time=${timeParam}`; } }; var NodeRuntimeInfo = class extends PluginRuntimeInfo { get version() { return Promise.resolve(import_github.default.context.sha); } get runUrl() { return import_github.default.context.payload.repository ? `${import_github.default.context.payload.repository?.html_url}/actions/runs/${import_github.default.context.runId}` : "http://localhost"; } }; // src/util.ts var import_ubiquity_os_logger = require("@ubiquity-os/ubiquity-os-logger"); // src/constants.ts var KERNEL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs96DOU+JqM8SyNXOB6u3 uBKIFiyrcST/LZTYN6y7LeJlyCuGPqSDrWCfjU9Ph5PLf9TWiNmeM8DGaOpwEFC7 U3NRxOSglo4plnQ5zRwIHHXvxyK400sQP2oISXymISuBQWjEIqkC9DybQrKwNzf+ I0JHWPqmwMIw26UvVOtXGOOWBqTkk+N2+/9f8eDIJP5QQVwwszc8s1rXOsLMlVIf wShw7GO4E2jyK8TSJKpyjV8eb1JJMDwFhPiRrtZfQJUtDf2mV/67shQww61BH2Y/ Plnalo58kWIbkqZoq1yJrL5sFb73osM5+vADTXVn79bkvea7W19nSkdMiarYt4Hq JQIDAQAB -----END PUBLIC KEY----- `; // src/util.ts function sanitizeMetadata(obj) { return JSON.stringify(obj, null, 2).replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;"); } function getPluginOptions(options) { return { // Important to use || and not ?? to not consider empty strings kernelPublicKey: options?.kernelPublicKey || KERNEL_PUBLIC_KEY, logLevel: options?.logLevel || import_ubiquity_os_logger.LOG_LEVEL.INFO, postCommentOnError: options?.postCommentOnError ?? true, settingsSchema: options?.settingsSchema, envSchema: options?.envSchema, commandSchema: options?.commandSchema, bypassSignatureVerification: options?.bypassSignatureVerification || false }; } // src/comment.ts var CommentHandler = class _CommentHandler { static HEADER_NAME = "UbiquityOS"; _lastCommentId = { reviewCommentId: null, issueCommentId: null }; async _updateIssueComment(context2, params) { if (!this._lastCommentId.issueCommentId) { throw context2.logger.error("issueCommentId is missing"); } const commentData = await context2.octokit.rest.issues.updateComment({ owner: params.owner, repo: params.repo, comment_id: this._lastCommentId.issueCommentId, body: params.body }); return { ...commentData.data, issueNumber: params.issueNumber }; } async _updateReviewComment(context2, params) { if (!this._lastCommentId.reviewCommentId) { throw context2.logger.error("reviewCommentId is missing"); } const commentData = await context2.octokit.rest.pulls.updateReviewComment({ owner: params.owner, repo: params.repo, comment_id: this._lastCommentId.reviewCommentId, body: params.body }); return { ...commentData.data, issueNumber: params.issueNumber }; } async _createNewComment(context2, params) { if (params.commentId) { const commentData2 = await context2.octokit.rest.pulls.createReplyForReviewComment({ owner: params.owner, repo: params.repo, pull_number: params.issueNumber, comment_id: params.commentId, body: params.body }); this._lastCommentId.reviewCommentId = commentData2.data.id; return { ...commentData2.data, issueNumber: params.issueNumber }; } const commentData = await context2.octokit.rest.issues.createComment({ owner: params.owner, repo: params.repo, issue_number: params.issueNumber, body: params.body }); this._lastCommentId.issueCommentId = commentData.data.id; return { ...commentData.data, issueNumber: params.issueNumber }; } _getIssueNumber(context2) { if ("issue" in context2.payload) return context2.payload.issue.number; if ("pull_request" in context2.payload) return context2.payload.pull_request.number; if ("discussion" in context2.payload) return context2.payload.discussion.number; return void 0; } _getCommentId(context2) { return "pull_request" in context2.payload && "comment" in context2.payload ? context2.payload.comment.id : void 0; } _extractIssueContext(context2) { if (!("repository" in context2.payload) || !context2.payload.repository?.owner?.login) { return null; } const issueNumber = this._getIssueNumber(context2); if (!issueNumber) return null; return { issueNumber, commentId: this._getCommentId(context2), owner: context2.payload.repository.owner.login, repo: context2.payload.repository.name }; } async _processMessage(context2, message) { if (message instanceof Error) { const metadata2 = { message: message.message, name: message.name, stack: message.stack }; return { metadata: metadata2, logMessage: context2.logger.error(message.message).logMessage }; } const metadata = message.metadata ? { ...message.metadata, message: message.metadata.message, stack: message.metadata.stack || message.metadata.error?.stack, caller: message.metadata.caller || message.metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1] } : { ...message }; return { metadata, logMessage: message.logMessage }; } _getInstigatorName(context2) { if ("installation" in context2.payload && context2.payload.installation && "account" in context2.payload.installation && context2.payload.installation?.account?.name) { return context2.payload.installation?.account?.name; } return context2.payload.sender?.login || _CommentHandler.HEADER_NAME; } async _createMetadataContent(context2, metadata) { const jsonPretty = sanitizeMetadata(metadata); const instigatorName = this._getInstigatorName(context2); const runUrl = PluginRuntimeInfo.getInstance().runUrl; const version = await PluginRuntimeInfo.getInstance().version; const callingFnName = metadata.caller || "anonymous"; return { header: `<!-- ${_CommentHandler.HEADER_NAME} - ${callingFnName} - ${version} - @${instigatorName} - ${runUrl}`, jsonPretty }; } _formatMetadataContent(logMessage, header, jsonPretty) { const metadataVisible = ["```json", jsonPretty, "```"].join("\n"); const metadataHidden = [header, jsonPretty, "-->"].join("\n"); return logMessage?.type === "fatal" ? [metadataVisible, metadataHidden].join("\n") : metadataHidden; } async _createCommentBody(context2, message, options) { const { metadata, logMessage } = await this._processMessage(context2, message); const { header, jsonPretty } = await this._createMetadataContent(context2, metadata); const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty); return `${options.raw ? logMessage?.raw : logMessage?.diff} ${metadataContent} `; } async postComment(context2, message, options = { updateComment: true, raw: false }) { const issueContext = this._extractIssueContext(context2); if (!issueContext) { context2.logger.info("Cannot post comment: missing issue context in payload"); return null; } const body = await this._createCommentBody(context2, message, options); const { issueNumber, commentId, owner, repo } = issueContext; const params = { owner, repo, body, issueNumber }; if (options.updateComment) { if (this._lastCommentId.issueCommentId && !("pull_request" in context2.payload && "comment" in context2.payload)) { return this._updateIssueComment(context2, params); } if (this._lastCommentId.reviewCommentId && "pull_request" in context2.payload && "comment" in context2.payload) { return this._updateReviewComment(context2, params); } } return this._createNewComment(context2, { ...params, commentId }); } }; // src/error.ts var import_ubiquity_os_logger2 = require("@ubiquity-os/ubiquity-os-logger"); function transformError(context2, error) { let loggerError; if (error instanceof AggregateError) { loggerError = context2.logger.error( error.errors.map((err) => { if (err instanceof import_ubiquity_os_logger2.LogReturn) { return err.logMessage.raw; } else if (err instanceof Error) { return err.message; } else { return err; } }).join("\n\n"), { error } ); } else if (error instanceof Error || error instanceof import_ubiquity_os_logger2.LogReturn) { loggerError = error; } else { loggerError = context2.logger.error(String(error)); } return loggerError; } // src/octokit.ts var import_core = require("@octokit/core"); var import_plugin_paginate_rest = require("@octokit/plugin-paginate-rest"); var import_plugin_rest_endpoint_methods = require("@octokit/plugin-rest-endpoint-methods"); var import_plugin_retry = require("@octokit/plugin-retry"); var import_plugin_throttling = require("@octokit/plugin-throttling"); var import_plugin_paginate_graphql = require("@octokit/plugin-paginate-graphql"); var defaultOptions = { throttle: { onAbuseLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Abuse limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; }, onRateLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; }, onSecondaryRateLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Secondary rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; } } }; var customOctokit = import_core.Octokit.plugin(import_plugin_throttling.throttling, import_plugin_retry.retry, import_plugin_paginate_rest.paginateRest, import_plugin_rest_endpoint_methods.restEndpointMethods, import_plugin_paginate_graphql.paginateGraphQL).defaults((instanceOptions) => { return { ...defaultOptions, ...instanceOptions }; }); // src/signature.ts async function verifySignature(publicKeyPem, inputs, signature) { try { const inputsOrdered = { stateId: inputs.stateId, eventName: inputs.eventName, eventPayload: inputs.eventPayload, settings: inputs.settings, authToken: inputs.authToken, ref: inputs.ref, command: inputs.command }; const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim(); const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0)); const publicKey = await crypto.subtle.importKey( "spki", binaryDer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"] ); const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); const dataArray = new TextEncoder().encode(JSON.stringify(inputsOrdered)); return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray); } catch (error) { console.error(error); return false; } } // src/types/input-schema.ts var import_typebox3 = require("@sinclair/typebox"); // src/types/command.ts var import_typebox = require("@sinclair/typebox"); var commandCallSchema = import_typebox.Type.Union([import_typebox.Type.Null(), import_typebox.Type.Object({ name: import_typebox.Type.String(), parameters: import_typebox.Type.Unknown() })]); // src/types/util.ts var import_typebox2 = require("@sinclair/typebox"); var import_value = require("@sinclair/typebox/value"); function jsonType(type) { return import_typebox2.Type.Transform(import_typebox2.Type.String()).Decode((value) => { const parsed = JSON.parse(value); return import_value.Value.Decode(type, import_value.Value.Default(type, parsed)); }).Encode((value) => JSON.stringify(value)); } // src/types/input-schema.ts var inputSchema = import_typebox3.Type.Object({ stateId: import_typebox3.Type.String(), eventName: import_typebox3.Type.String(), eventPayload: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any())), command: jsonType(commandCallSchema), authToken: import_typebox3.Type.String(), settings: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any())), ref: import_typebox3.Type.String(), signature: import_typebox3.Type.String() }); // src/server.ts function createPlugin(handler, manifest, options) { const pluginOptions = getPluginOptions(options); const app = new import_hono.Hono(); app.get("/manifest.json", (ctx) => { return ctx.json(manifest); }); app.post("/", async function appPost(ctx) { if (ctx.req.header("content-type") !== "application/json") { throw new import_http_exception.HTTPException(400, { message: "Content-Type must be application/json" }); } const body = await ctx.req.json(); const inputSchemaErrors = [...import_value2.Value.Errors(inputSchema, body)]; if (inputSchemaErrors.length) { console.dir(inputSchemaErrors, { depth: null }); throw new import_http_exception.HTTPException(400, { message: "Invalid body" }); } const signature = body.signature; if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) { throw new import_http_exception.HTTPException(400, { message: "Invalid signature" }); } const inputs = import_value2.Value.Decode(inputSchema, body); let config2; if (pluginOptions.settingsSchema) { try { config2 = import_value2.Value.Decode(pluginOptions.settingsSchema, import_value2.Value.Default(pluginOptions.settingsSchema, inputs.settings)); } catch (e) { console.dir(...import_value2.Value.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null }); throw e; } } else { config2 = inputs.settings; } let env; const honoEnvironment = (0, import_adapter2.env)(ctx); if (pluginOptions.envSchema) { try { env = import_value2.Value.Decode(pluginOptions.envSchema, import_value2.Value.Default(pluginOptions.envSchema, honoEnvironment)); } catch (e) { console.dir(...import_value2.Value.Errors(pluginOptions.envSchema, honoEnvironment), { depth: null }); throw e; } } else { env = ctx.env; } const workerName = new URL(inputs.ref).hostname.split(".")[0]; PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName }); let command = null; if (inputs.command && pluginOptions.commandSchema) { try { command = import_value2.Value.Decode(pluginOptions.commandSchema, import_value2.Value.Default(pluginOptions.commandSchema, inputs.command)); } catch (e) { console.log(...import_value2.Value.Errors(pluginOptions.commandSchema, inputs.command), { depth: null }); throw e; } } else if (inputs.command) { command = inputs.command; } const context2 = { eventName: inputs.eventName, payload: inputs.eventPayload, command, octokit: new customOctokit({ auth: inputs.authToken }), config: config2, env, logger: new import_ubiquity_os_logger3.Logs(pluginOptions.logLevel), commentHandler: new CommentHandler() }; try { const result = await handler(context2); return ctx.json({ stateId: inputs.stateId, output: result ?? {} }); } catch (error) { console.error(error); const loggerError = transformError(context2, error); if (pluginOptions.postCommentOnError && loggerError) { await context2.commentHandler.postComment(context2, loggerError); } throw new import_http_exception.HTTPException(500, { message: "Unexpected error" }); } }); return app; } // src/actions.ts var core = __toESM(require("@actions/core")); var github2 = __toESM(require("@actions/github")); var import_value3 = require("@sinclair/typebox/value"); var import_ubiquity_os_logger4 = require("@ubiquity-os/ubiquity-os-logger"); var import_dotenv = require("dotenv"); (0, import_dotenv.config)(); async function createActionsPlugin(handler, options) { const pluginOptions = getPluginOptions(options); const pluginGithubToken = process.env.PLUGIN_GITHUB_TOKEN; if (!pluginGithubToken) { core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set"); return; } const body = github2.context.payload.inputs; const inputSchemaErrors = [...import_value3.Value.Errors(inputSchema, body)]; if (inputSchemaErrors.length) { console.dir(inputSchemaErrors, { depth: null }); core.setFailed(`Error: Invalid inputs payload: ${inputSchemaErrors.map((o) => o.message).join(", ")}`); return; } const signature = body.signature; if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) { core.setFailed(`Error: Invalid signature`); return; } const inputs = import_value3.Value.Decode(inputSchema, body); let config2; if (pluginOptions.settingsSchema) { try { config2 = import_value3.Value.Decode(pluginOptions.settingsSchema, import_value3.Value.Default(pluginOptions.settingsSchema, inputs.settings)); } catch (e) { console.dir(...import_value3.Value.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null }); core.setFailed(`Error: Invalid settings provided.`); throw e; } } else { config2 = inputs.settings; } let env; if (pluginOptions.envSchema) { try { env = import_value3.Value.Decode(pluginOptions.envSchema, import_value3.Value.Default(pluginOptions.envSchema, process.env)); } catch (e) { console.dir(...import_value3.Value.Errors(pluginOptions.envSchema, process.env), { depth: null }); core.setFailed(`Error: Invalid environment provided.`); throw e; } } else { env = process.env; } let command = null; if (inputs.command && pluginOptions.commandSchema) { try { command = import_value3.Value.Decode(pluginOptions.commandSchema, import_value3.Value.Default(pluginOptions.commandSchema, inputs.command)); } catch (e) { console.dir(...import_value3.Value.Errors(pluginOptions.commandSchema, inputs.command), { depth: null }); throw e; } } else if (inputs.command) { command = inputs.command; } const context2 = { eventName: inputs.eventName, payload: inputs.eventPayload, command, octokit: new customOctokit({ auth: inputs.authToken }), config: config2, env, logger: new import_ubiquity_os_logger4.Logs(pluginOptions.logLevel), commentHandler: new CommentHandler() }; try { const result = await handler(context2); core.setOutput("result", result); await returnDataToKernel(pluginGithubToken, inputs.stateId, result); } catch (error) { console.error(error); const loggerError = transformError(context2, error); if (loggerError instanceof import_ubiquity_os_logger4.LogReturn) { core.setFailed(loggerError.logMessage.diff); } else if (loggerError instanceof Error) { core.setFailed(loggerError); } if (pluginOptions.postCommentOnError && loggerError) { await context2.commentHandler.postComment(context2, loggerError); } } } async function returnDataToKernel(repoToken, stateId, output) { const octokit = new customOctokit({ auth: repoToken }); await octokit.rest.repos.createDispatchEvent({ owner: github2.context.repo.owner, repo: github2.context.repo.repo, event_type: "return-data-to-ubiquity-os-kernel", client_payload: { state_id: stateId, output: output ? JSON.stringify(output) : null } }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CommentHandler, createActionsPlugin, createPlugin });