@ubiquity-os/plugin-sdk
Version:
SDK for plugin support.
562 lines (544 loc) • 23 kB
JavaScript
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, "<").replace(/>/g, ">").replace(/--/g, "--");
}
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
});
;