diff --git a/webapp/event.ts b/common/event.ts similarity index 100% rename from webapp/event.ts rename to common/event.ts diff --git a/common/manifest.ts b/common/manifest.ts new file mode 100644 index 0000000..f492ce2 --- /dev/null +++ b/common/manifest.ts @@ -0,0 +1,23 @@ +import * as plugbox from "../plugbox/types"; +import { EndpointHook } from "../plugbox/types"; + +export type CommandDef = { + // Function name to invoke + invoke: string; + + // Bind to keyboard shortcut + key?: string; + mac?: string; + + // If to show in slash invoked menu and if so, with what label + // should match slashCommandRegexp + slashCommand?: string; +}; + +export type SilverBulletHooks = { + commands?: { + [key: string]: CommandDef; + }; +} & plugbox.EndpointHook; + +export type Manifest = plugbox.Manifest; diff --git a/mobile/html/index.html b/mobile/html/index.html index 0d5633e..7bb37d2 100644 --- a/mobile/html/index.html +++ b/mobile/html/index.html @@ -6,14 +6,14 @@ - +
diff --git a/package.json b/package.json index 6e49ee5..49ce269 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "watch": "parcel watch", "build": "parcel build", "clean": "rm -rf dist", - "plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json webapp/generated/core.plug.json", - "server": "nodemon dist/server/server.js pages", + "plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json plugs/dist/core.plug.json", + "server": "nodemon -w dist/server dist/server/server.js pages", "test": "jest" }, "targets": { @@ -40,6 +40,7 @@ "test": { "source": [ "plugbox/runtime.test.ts", + "plugbox/endpoint.test.ts", "server/api.test.ts" ], "outputFormat": "commonjs", @@ -75,12 +76,14 @@ "jest": "^27.5.1", "knex": "^1.0.4", "lodash": "^4.17.21", + "node-cron": "^3.0.0", "nodemon": "^2.0.15", "parcel": "^2.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", "socket.io": "^4.4.1", "socket.io-client": "^4.4.1", + "supertest": "^6.2.2", "ts-jest": "^27.1.3", "vm2": "^3.9.9", "yargs": "^17.3.1" @@ -95,8 +98,10 @@ "@types/events": "^3.0.0", "@types/jest": "^27.4.1", "@types/node": "^17.0.21", + "@types/node-cron": "^3.0.1", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", + "@types/supertest": "^2.0.11", "@vscode/sqlite3": "^5.0.7", "assert": "^2.0.0", "crypto-browserify": "^3.12.0", diff --git a/plugbox/bin/plugbox-bundle.ts b/plugbox/bin/plugbox-bundle.ts index 814e947..6e4d1eb 100755 --- a/plugbox/bin/plugbox-bundle.ts +++ b/plugbox/bin/plugbox-bundle.ts @@ -6,9 +6,9 @@ import path from "path"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import {Manifest} from "../types"; +import { Manifest } from "../types"; -async function compile(filePath : string, functionName : string, debug: boolean) { +async function compile(filePath: string, functionName: string, debug: boolean) { let outFile = "out.js"; let inFile = filePath; @@ -47,7 +47,9 @@ export default ${functionName};` async function bundle(manifestPath: string, sourceMaps: boolean) { const rootPath = path.dirname(manifestPath); - const manifest = JSON.parse((await readFile(manifestPath)).toString()) as Manifest; + const manifest = JSON.parse( + (await readFile(manifestPath)).toString() + ) as Manifest; for (let [name, def] of Object.entries(manifest.functions)) { let jsFunctionName = "default", @@ -69,7 +71,10 @@ async function run() { .parse(); let generatedManifest = await bundle(args._[0] as string, !!args.debug); - await writeFile(args._[1] as string, JSON.stringify(generatedManifest, null, 2)); + await writeFile( + args._[1] as string, + JSON.stringify(generatedManifest, null, 2) + ); } run().catch((e) => { diff --git a/plugbox/cron.ts b/plugbox/cron.ts new file mode 100644 index 0000000..81844f5 --- /dev/null +++ b/plugbox/cron.ts @@ -0,0 +1,11 @@ +import { System } from "./runtime"; +import { CronHook } from "./types"; +import cron from "node-cron"; + +export function cronSystem(system: System) { + let task = cron.schedule("* * * * *", () => { + + }); + // @ts-ignore + task.destroy(); +} diff --git a/plugbox/endpoint.test.ts b/plugbox/endpoint.test.ts new file mode 100644 index 0000000..df3081f --- /dev/null +++ b/plugbox/endpoint.test.ts @@ -0,0 +1,47 @@ +import { createSandbox } from "./node_sandbox"; +import { System } from "./runtime"; +import { test, expect } from "@jest/globals"; +import { EndPointDef, EndpointHook, Manifest } from "./types"; +import express from "express"; +import request from "supertest"; +import { exposeSystem } from "./endpoints"; + +test("Run a plugbox endpoint server", async () => { + let system = new System(); + let plug = await system.load( + "test", + { + functions: { + testhandler: { + code: `(() => { + return { + default: (req) => { + console.log("Req", req); + return {status: 200, body: [1, 2, 3], headers: {"Content-type": "application/json"}}; + } + }; + })()`, + }, + }, + hooks: { + endpoints: [{ method: "GET", path: "/", handler: "testhandler" }], + }, + } as Manifest, + createSandbox(system) + ); + + const app = express(); + const port = 3123; + app.use(exposeSystem(system)); + let server = app.listen(port, () => { + console.log(`Listening on port ${port}`); + }); + let resp = await request(app) + .get("/_/test/?name=Pete") + .expect((resp) => { + expect(resp.status).toBe(200); + expect(resp.header["content-type"]).toContain("application/json"); + expect(resp.text).toBe(JSON.stringify([1, 2, 3])); + }); + server.close(); +}); diff --git a/plugbox/endpoints.ts b/plugbox/endpoints.ts new file mode 100644 index 0000000..e09b5f2 --- /dev/null +++ b/plugbox/endpoints.ts @@ -0,0 +1,85 @@ +import { System } from "./runtime"; +import { EndpointHook } from "./types"; +import express from "express"; + +export type EndpointRequest = { + method: string; + path: string; + query: { [key: string]: string }; + headers: { [key: string]: string }; + body: any; +}; + +export type EndpointResponse = { + status: number; + headers?: { [key: string]: string }; + body: any; +}; + +const endPointPrefix = "/_"; + +export function exposeSystem(system: System) { + return ( + req: express.Request, + res: express.Response, + next: express.NextFunction + ) => { + if (!req.path.startsWith(endPointPrefix)) { + return next(); + } + Promise.resolve() + .then(async () => { + for (const [plugName, plug] of system.loadedPlugs.entries()) { + const manifest = plug.manifest; + if (!manifest) { + continue; + } + const endpoints = manifest.hooks?.endpoints; + if (endpoints) { + let prefix = `${endPointPrefix}/${plugName}`; + if (!req.path.startsWith(prefix)) { + continue; + } + for (const { path, method, handler } of endpoints) { + let prefixedPath = `${prefix}${path}`; + if (prefixedPath === req.path && method === req.method) { + try { + const response: EndpointResponse = await plug.invoke( + handler, + [ + { + path: req.path, + method: req.method, + body: req.body, + query: req.query, + headers: req.headers, + } as EndpointRequest, + ] + ); + let resp = res.status(response.status); + if (response.headers) { + for (const [key, value] of Object.entries( + response.headers + )) { + resp = resp.header(key, value); + } + } + resp.send(response.body); + return; + } catch (e: any) { + console.error("Error executing function", e); + res.status(500).send(e.message); + return; + } + } + } + } + } + next(); + }) + .catch((e) => { + console.error(e); + next(e); + }); + }; +} diff --git a/plugbox/iframe_sandbox.ts b/plugbox/iframe_sandbox.ts index ddaadaf..53cfa53 100644 --- a/plugbox/iframe_sandbox.ts +++ b/plugbox/iframe_sandbox.ts @@ -8,6 +8,7 @@ import sandboxHtml from "bundle-text:./iframe_sandbox.html"; class IFrameWrapper implements WorkerLike { private iframe: HTMLIFrameElement; onMessage?: (message: any) => Promise; + ready: Promise; constructor() { const iframe = document.createElement("iframe", {}); @@ -27,6 +28,12 @@ class IFrameWrapper implements WorkerLike { }); }); document.body.appendChild(iframe); + this.ready = new Promise((resolve) => { + iframe.onload = () => { + resolve(); + iframe.onload = null; + }; + }); } postMessage(message: any): void { diff --git a/plugbox/node_sandbox.ts b/plugbox/node_sandbox.ts index 129a169..e03c8b7 100644 --- a/plugbox/node_sandbox.ts +++ b/plugbox/node_sandbox.ts @@ -6,14 +6,12 @@ import * as fs from "fs"; import { safeRun } from "./util"; // @ts-ignore -import workerCode from "bundle-text:./node_worker.ts" - -// ParcelJS will simply inline this into the bundle. -// const workerCode = fs.readFileSync(__dirname + "/node_worker.ts", "utf-8"); +import workerCode from "bundle-text:./node_worker.ts"; class NodeWorkerWrapper implements WorkerLike { onMessage?: (message: any) => Promise; private worker: Worker; + ready: Promise; constructor(worker: Worker) { this.worker = worker; @@ -22,6 +20,9 @@ class NodeWorkerWrapper implements WorkerLike { await this.onMessage!(message); }); }); + this.ready = new Promise((resolve) => { + worker.once("online", resolve); + }); } postMessage(message: any): void { @@ -34,6 +35,9 @@ class NodeWorkerWrapper implements WorkerLike { } export function createSandbox(system: System) { + let worker = new Worker(workerCode, { + eval: true, + }); return new Sandbox( system, new NodeWorkerWrapper( diff --git a/plugbox/node_worker.ts b/plugbox/node_worker.ts index e61d409..0df9df1 100644 --- a/plugbox/node_worker.ts +++ b/plugbox/node_worker.ts @@ -3,18 +3,18 @@ const { parentPort } = require("worker_threads"); let loadedFunctions = new Map(); let pendingRequests = new Map< - number, - { - resolve: (result: unknown) => void; - reject: (e: any) => void; - } - >(); + number, + { + resolve: (result: unknown) => void; + reject: (e: any) => void; + } +>(); let vm = new VM({ sandbox: { console: console, self: { - syscall: (reqId : number, name : string, args: any[]) => { + syscall: (reqId: number, name: string, args: any[]) => { return new Promise((resolve, reject) => { pendingRequests.set(reqId, { resolve, reject }); parentPort.postMessage({ @@ -30,17 +30,17 @@ let vm = new VM({ }, }); -function wrapScript(code : string) { +function wrapScript(code: string) { return `(${code})["default"]`; } -function safeRun(fn : any) { - fn().catch((e : any) => { +function safeRun(fn: any) { + fn().catch((e: any) => { console.error(e); }); } -parentPort.on("message", (data : any) => { +parentPort.on("message", (data: any) => { safeRun(async () => { switch (data.type) { case "load": @@ -62,9 +62,10 @@ parentPort.on("message", (data : any) => { parentPort.postMessage({ type: "result", id: data.id, - result: result, + // TOOD: Figure out if this is necessary, because it's expensive + result: result && JSON.parse(JSON.stringify(result)), }); - } catch (e : any) { + } catch (e: any) { // console.log("ERROR", e); parentPort.postMessage({ type: "result", diff --git a/plugbox/plug_loader.ts b/plugbox/plug_loader.ts new file mode 100644 index 0000000..f5e6c9b --- /dev/null +++ b/plugbox/plug_loader.ts @@ -0,0 +1,71 @@ +import fs, { stat, watch } from "fs/promises"; +import path from "path"; +import { createSandbox } from "./node_sandbox"; +import { System } from "./runtime"; +import { safeRun } from "../server/util"; + +function extractPlugName(localPath: string): string { + const baseName = path.basename(localPath); + return baseName.substring(0, baseName.length - ".plug.json".length); +} + +export class DiskPlugLoader { + private system: System; + private plugPath: string; + + constructor(system: System, plugPath: string) { + this.system = system; + this.plugPath = plugPath; + } + + watcher() { + safeRun(async () => { + for await (const { filename, eventType } of watch(this.plugPath, { + recursive: true, + })) { + if (!filename.endsWith(".plug.json")) { + return; + } + try { + let localPath = path.join(this.plugPath, filename); + const plugName = extractPlugName(localPath); + try { + await fs.stat(localPath); + } catch (e) { + // Likely removed + await this.system.unload(plugName); + this.system.emit("plugRemoved", plugName); + } + const plugDef = await this.loadPlugFromFile(localPath); + this.system.emit("plugUpdated", plugName, plugDef); + } catch { + // ignore, error handled by loadPlug + } + } + }); + } + + private async loadPlugFromFile(localPath: string) { + const plug = await fs.readFile(localPath, "utf8"); + const plugName = extractPlugName(localPath); + + console.log("Now loading plug", plugName); + try { + const plugDef = JSON.parse(plug); + await this.system.load(plugName, plugDef, createSandbox(this.system)); + return plugDef; + } catch (e) { + console.error("Could not parse plugin file", e); + throw e; + } + } + + async loadPlugs() { + for (let filename of await fs.readdir(this.plugPath)) { + if (filename.endsWith(".plug.json")) { + let localPath = path.join(this.plugPath, filename); + await this.loadPlugFromFile(localPath); + } + } + } +} diff --git a/plugbox/runtime.test.ts b/plugbox/runtime.test.ts index 2b00d35..16f5c81 100644 --- a/plugbox/runtime.test.ts +++ b/plugbox/runtime.test.ts @@ -75,5 +75,5 @@ test("Run a Node sandbox", async () => { } catch (e: any) { expect(e.message).toBe("#fail"); } - await system.stop(); + await system.unloadAll(); }); diff --git a/plugbox/runtime.ts b/plugbox/runtime.ts index bf01e9c..7434c01 100644 --- a/plugbox/runtime.ts +++ b/plugbox/runtime.ts @@ -4,6 +4,7 @@ import { WorkerLike, WorkerMessage, } from "./types"; +import { EventEmitter } from "../common/event"; interface SysCallMapping { [key: string]: (...args: any) => Promise | any; @@ -31,6 +32,7 @@ export class Sandbox { } async load(name: string, code: string): Promise { + await this.worker.ready; this.worker.postMessage({ type: "load", name: name, @@ -119,18 +121,20 @@ export class Plug { if (!funDef) { throw new Error(`Function ${name} not found in manifest`); } - await this.sandbox.load(name, this.manifest!.functions[name].code!); + await this.sandbox.load(name, funDef.code!); } return await this.sandbox.invoke(name, args); } async dispatchEvent(name: string, data?: any): Promise { + if (!this.manifest!.hooks?.events) { + return []; + } let functionsToSpawn = this.manifest!.hooks.events[name]; if (functionsToSpawn) { return await Promise.all( - functionsToSpawn.map( - async (functionToSpawn: string) => - await this.invoke(functionToSpawn, [data]) + functionsToSpawn.map((functionToSpawn: string) => + this.invoke(functionToSpawn, [data]) ) ); } else { @@ -143,10 +147,21 @@ export class Plug { } } -export class System { +export type SystemJSON = { [key: string]: Manifest }; + +export type SystemEvents = { + plugUpdated: (name: string, plug: Plug) => void; + plugRemoved: (name: string) => void; +}; + +export class System extends EventEmitter> { protected plugs = new Map>(); registeredSyscalls: SysCallMapping = {}; + constructor() { + super(); + } + registerSyscalls(...registrationObjects: SysCallMapping[]) { for (const registrationObject of registrationObjects) { for (let p in registrationObject) { @@ -171,23 +186,63 @@ export class System { manifest: Manifest, sandbox: Sandbox ): Promise> { + if (this.plugs.has(name)) { + await this.unload(name); + } const plug = new Plug(this, name, sandbox); await plug.load(manifest); this.plugs.set(name, plug); return plug; } + async unload(name: string) { + const plug = this.plugs.get(name); + if (!plug) { + throw Error(`Plug ${name} not found`); + } + await plug.stop(); + this.plugs.delete(name); + } + async dispatchEvent(name: string, data?: any): Promise { let promises = []; for (let plug of this.plugs.values()) { - promises.push(plug.dispatchEvent(name, data)); + for (let result of await plug.dispatchEvent(name, data)) { + promises.push(result); + } } return await Promise.all(promises); } - async stop(): Promise { + get loadedPlugs(): Map> { + return this.plugs; + } + + toJSON(): SystemJSON { + let plugJSON: { [key: string]: Manifest } = {}; + for (let [name, plug] of this.plugs) { + if (!plug.manifest) { + continue; + } + plugJSON[name] = plug.manifest; + } + return plugJSON; + } + + async replaceAllFromJSON( + json: SystemJSON, + sandboxFactory: () => Sandbox + ) { + await this.unloadAll(); + for (let [name, manifest] of Object.entries(json)) { + console.log("Loading plug", name); + await this.load(name, manifest, sandboxFactory()); + } + } + + async unloadAll(): Promise { return Promise.all( - Array.from(this.plugs.values()).map((plug) => plug.stop()) + Array.from(this.plugs.keys()).map(this.unload.bind(this)) ); } } diff --git a/plugbox/sandbox_worker.ts b/plugbox/sandbox_worker.ts index 2415726..f46d28e 100644 --- a/plugbox/sandbox_worker.ts +++ b/plugbox/sandbox_worker.ts @@ -17,9 +17,7 @@ declare global { let postMessage = self.postMessage.bind(self); if (window.parent !== window) { - console.log("running in an iframe"); postMessage = window.parent.postMessage.bind(window.parent); - // postMessage({ type: "test" }, "*"); } self.syscall = async (id: number, name: string, args: any[]) => { @@ -43,7 +41,6 @@ return fn["default"].apply(null, arguments);`; } self.addEventListener("message", (event: { data: WorkerMessage }) => { - // console.log("Got a message", event.data); safeRun(async () => { let messageEvent = event; let data = messageEvent.data; diff --git a/plugbox/types.ts b/plugbox/types.ts index f50b3ec..e0a6681 100644 --- a/plugbox/types.ts +++ b/plugbox/types.ts @@ -1,7 +1,3 @@ -export type EventHook = { - events: { [key: string]: string[] }; -}; - export type WorkerMessageType = "load" | "invoke" | "syscall-response"; export type WorkerMessage = { @@ -37,7 +33,30 @@ export interface FunctionDef { code?: string; } +export type EventHook = { + events?: { [key: string]: string[] }; +}; + +export type EndpointHook = { + endpoints?: EndPointDef[]; +}; +export type EndPointDef = { + method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; + path: string; + handler: string; // function name +}; + +export type CronHook = { + crons?: CronDef[]; +}; + +export type CronDef = { + cron: string; + handler: string; // function name +}; + export interface WorkerLike { + ready: Promise; onMessage?: (message: any) => Promise; postMessage(message: any): void; terminate(): void; diff --git a/plugbox/webworker_sandbox.ts b/plugbox/webworker_sandbox.ts index 72ae57a..fd68179 100644 --- a/plugbox/webworker_sandbox.ts +++ b/plugbox/webworker_sandbox.ts @@ -5,6 +5,7 @@ import { safeRun } from "./util"; class WebWorkerWrapper implements WorkerLike { private worker: Worker; onMessage?: (message: any) => Promise; + ready: Promise; constructor(worker: Worker) { this.worker = worker; @@ -15,6 +16,7 @@ class WebWorkerWrapper implements WorkerLike { await this.onMessage!(data); }); }); + this.ready = Promise.resolve(); } postMessage(message: any): void { this.worker.postMessage(message); diff --git a/plugs/core/core.plug.json b/plugs/core/core.plug.json index fadf7d8..bb22533 100644 --- a/plugs/core/core.plug.json +++ b/plugs/core/core.plug.json @@ -36,8 +36,16 @@ "events": { "page:click": ["taskToggle", "clickNavigate"], "editor:complete": ["pageComplete"], - "page:index": ["indexLinks"] - } + "page:index": ["indexLinks"], + "load": ["welcome"] + }, + "endpoints": [ + { + "method": "GET", + "path": "/", + "handler": "endpointTest" + } + ] }, "functions": { "indexLinks": { @@ -76,8 +84,11 @@ "toggle_h2": { "path": "./markup.ts:toggleH2" }, - "server_test": { - "path": "./server.ts:test" + "endpointTest": { + "path": "./server.ts:endpointTest" + }, + "welcome": { + "path": "./server.ts:welcome" } } } diff --git a/plugs/core/server.ts b/plugs/core/server.ts index b3f8f9a..3deaae5 100644 --- a/plugs/core/server.ts +++ b/plugs/core/server.ts @@ -1,5 +1,15 @@ -import { syscall } from "./lib/syscall"; -export function test() { - console.log("I'm running on the server!"); - return 5; +import { EndpointRequest, EndpointResponse } from "../../plugbox/endpoints"; + +export function endpointTest(req: EndpointRequest): EndpointResponse { + console.log("I'm running on the server!", req); + return { + status: 200, + body: "Hello world!", + }; +} + +export function welcome() { + for (var i = 0; i < 10; i++) { + console.log("Welcome to you all!!!"); + } } diff --git a/server/api.test.ts b/server/api.test.ts index 39dbe29..d15959f 100644 --- a/server/api.test.ts +++ b/server/api.test.ts @@ -6,6 +6,8 @@ import { Server } from "socket.io"; import { SocketServer } from "./api_server"; import * as path from "path"; import * as fs from "fs"; +import { SilverBulletHooks } from "../common/manifest"; +import { System } from "../plugbox/runtime"; describe("Server test", () => { let io: Server, @@ -38,7 +40,11 @@ describe("Server test", () => { const port = httpServer.address().port; // @ts-ignore clientSocket = new Client(`http://localhost:${port}`); - socketServer = new SocketServer(tmpDir, io); + socketServer = new SocketServer( + tmpDir, + io, + new System() + ); clientSocket.on("connect", done); await socketServer.init(); }); diff --git a/server/api_server.ts b/server/api_server.ts index 3096a1e..4b9c0a3 100644 --- a/server/api_server.ts +++ b/server/api_server.ts @@ -4,9 +4,7 @@ import * as path from "path"; import { IndexApi } from "./index_api"; import { PageApi } from "./page_api"; import { System } from "../plugbox/runtime"; -import { createSandbox } from "../plugbox/node_sandbox"; -import { NuggetHook } from "../webapp/types"; -import corePlug from "../webapp/generated/core.plug.json"; +import { SilverBulletHooks } from "../common/manifest"; import pageIndexSyscalls from "./syscalls/page_index"; export class ClientConnection { @@ -25,12 +23,16 @@ export class SocketServer { private apis = new Map(); readonly rootPath: string; private serverSocket: Server; - system: System; + system: System; - constructor(rootPath: string, serverSocket: Server) { + constructor( + rootPath: string, + serverSocket: Server, + system: System + ) { this.rootPath = path.resolve(rootPath); this.serverSocket = serverSocket; - this.system = new System(); + this.system = system; } async registerApi(name: string, apiProvider: ApiProvider) { @@ -52,12 +54,6 @@ export class SocketServer { ) ); - let plug = await this.system.load( - "core", - corePlug, - createSandbox(this.system) - ); - this.serverSocket.on("connection", (socket) => { const clientConn = new ClientConnection(socket); @@ -112,6 +108,9 @@ export class SocketServer { }); }); } + + console.log("Sending the sytem to the client"); + socket.emit("loadSystem", this.system.toJSON()); }); } diff --git a/server/express_server.ts b/server/express_server.ts new file mode 100644 index 0000000..d507b99 --- /dev/null +++ b/server/express_server.ts @@ -0,0 +1,35 @@ +import { Express } from "express"; +import { System } from "../plugbox/runtime"; +import { SilverBulletHooks } from "../common/manifest"; +import { exposeSystem } from "../plugbox/endpoints"; +import { readFile } from "fs/promises"; + +export class ExpressServer { + app: Express; + system: System; + private rootPath: string; + + constructor( + app: Express, + rootPath: string, + distDir: string, + system: System + ) { + this.app = app; + this.rootPath = rootPath; + this.system = system; + + app.use(exposeSystem(this.system)); + + // Fallback, serve index.html + let cachedIndex: string | undefined = undefined; + app.get("/*", async (req, res) => { + if (!cachedIndex) { + cachedIndex = await readFile(`${distDir}/index.html`, "utf8"); + } + res.status(200).header("Content-Type", "text/html").send(cachedIndex); + }); + } + + async init() {} +} diff --git a/server/page_api.ts b/server/page_api.ts index 02d3d5e..1b202e3 100644 --- a/server/page_api.ts +++ b/server/page_api.ts @@ -10,20 +10,20 @@ import path from "path"; import { stat } from "fs/promises"; import { Cursor, cursorEffect } from "../webapp/cursorEffect"; import { System } from "../plugbox/runtime"; -import { NuggetHook } from "../webapp/types"; +import { SilverBulletHooks } from "../common/manifest"; export class PageApi implements ApiProvider { openPages: Map; pageStore: DiskStorage; rootPath: string; connectedSockets: Set; - private system: System; + private system: System; constructor( rootPath: string, connectedSockets: Set, openPages: Map, - system: System + system: System ) { this.pageStore = new DiskStorage(rootPath); this.rootPath = rootPath; @@ -34,6 +34,20 @@ export class PageApi implements ApiProvider { async init(): Promise { this.fileWatcher(); + this.system.on({ + plugUpdated: (plugName, plugDef) => { + console.log("Plug updated on disk, broadcasting to all clients"); + this.connectedSockets.forEach((socket) => { + socket.emit("plugUpdated", plugName, plugDef); + }); + }, + plugRemoved: (plugName) => { + console.log("Plug removed on disk, broadcasting to all clients"); + this.connectedSockets.forEach((socket) => { + socket.emit("plugRemoved", plugName); + }); + }, + }); } broadcastCursors(page: Page) { diff --git a/server/server.ts b/server/server.ts index 63903d7..4fc3210 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,10 +1,13 @@ import express from "express"; -import { readFile } from "fs/promises"; import http from "http"; import { Server } from "socket.io"; import { SocketServer } from "./api_server"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import { System } from "../plugbox/runtime"; +import { SilverBulletHooks } from "../common/manifest"; +import { ExpressServer } from "./express_server"; +import { DiskPlugLoader } from "../plugbox/plug_loader"; let args = yargs(hideBin(process.argv)) .option("debug", { @@ -16,8 +19,12 @@ let args = yargs(hideBin(process.argv)) }) .parse(); +const pagesPath = args._[0] as string; + const app = express(); const server = http.createServer(app); +const system = new System(); + const io = new Server(server, { cors: { methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE", @@ -29,18 +36,26 @@ const port = args.port; const distDir = `${__dirname}/../webapp`; app.use("/", express.static(distDir)); -let socketServer = new SocketServer(args._[0] as string, io); -socketServer.init(); -// Fallback, serve index.html -let cachedIndex: string | undefined = undefined; -app.get("/*", async (req, res) => { - if (!cachedIndex) { - cachedIndex = await readFile(`${distDir}/index.html`, "utf8"); - } - res.status(200).header("Content-Type", "text/html").send(cachedIndex); +let socketServer = new SocketServer(pagesPath, io, system); +socketServer.init().catch((e) => { + console.error(e); }); -server.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); +const expressServer = new ExpressServer(app, pagesPath, distDir, system); +expressServer + .init() + .then(async () => { + let plugLoader = new DiskPlugLoader( + system, + `${__dirname}/../../plugs/dist` + ); + await plugLoader.loadPlugs(); + plugLoader.watcher(); + server.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); + }) + .catch((e) => { + console.error(e); + }); diff --git a/server/syscalls/shell.ts b/server/syscalls/shell.ts new file mode 100644 index 0000000..5c3c1d6 --- /dev/null +++ b/server/syscalls/shell.ts @@ -0,0 +1,15 @@ +import { promisify } from "util"; +import { execFile } from "child_process"; + +const execFilePromise = promisify(execFile); + +export default function (cwd: string) { + return { + "shell.run": async (cmd: string, args: string[]) => { + let { stdout, stderr } = await execFilePromise(cmd, args, { + cwd: cwd, + }); + return { stdout, stderr }; + }, + }; +} diff --git a/webapp/boot.ts b/webapp/boot.ts index 02e02c9..5eb5166 100644 --- a/webapp/boot.ts +++ b/webapp/boot.ts @@ -17,5 +17,5 @@ window.editor = editor; navigator.serviceWorker .register(new URL("service_worker.ts", import.meta.url), { type: "module" }) .then((r) => { - console.log("Service worker registered", r); + // console.log("Service worker registered", r); }); diff --git a/webapp/collab.ts b/webapp/collab.ts index 0962bb4..f337e4b 100644 --- a/webapp/collab.ts +++ b/webapp/collab.ts @@ -17,7 +17,7 @@ import { } from "@codemirror/view"; import { throttle } from "./util"; import { Cursor, cursorEffect } from "./cursorEffect"; -import { EventEmitter } from "./event"; +import { EventEmitter } from "../common/event"; const throttleInterval = 250; diff --git a/webapp/editor.tsx b/webapp/editor.tsx index f34b328..b15357a 100644 --- a/webapp/editor.tsx +++ b/webapp/editor.tsx @@ -19,7 +19,6 @@ import { KeyBinding, keymap, } from "@codemirror/view"; -// import { debounce } from "lodash"; import React, { useEffect, useReducer } from "react"; import ReactDOM from "react-dom"; import { Plug, System } from "../plugbox/runtime"; @@ -31,7 +30,6 @@ import { CommandPalette } from "./components/command_palette"; import { PageNavigator } from "./components/page_navigator"; import { TopBar } from "./components/top_bar"; import { Cursor } from "./cursorEffect"; -import coreManifest from "./generated/core.plug.json"; import { lineWrapper } from "./line_wrapper"; import { markdown } from "./markdown"; import { IPageNavigator, PathPageNavigator } from "./navigator"; @@ -49,9 +47,9 @@ import { AppCommand, AppViewState, initialViewState, - NuggetHook, slashCommandRegexp, } from "./types"; +import { SilverBulletHooks } from "../common/manifest"; import { safeRun } from "./util"; class PageState { @@ -65,20 +63,19 @@ class PageState { } export class Editor implements AppEventDispatcher { + private system = new System(); editorView?: EditorView; viewState: AppViewState; viewDispatch: React.Dispatch; openPages: Map; space: Space; editorCommands: Map; - plugs: Plug[]; navigationResolve?: (val: undefined) => void; pageNavigator: IPageNavigator; constructor(space: Space, parent: Element) { this.editorCommands = new Map(); this.openPages = new Map(); - this.plugs = []; this.space = space; this.viewState = initialViewState; this.viewDispatch = () => {}; @@ -91,6 +88,13 @@ export class Editor implements AppEventDispatcher { parent: document.getElementById("editor")!, }); this.pageNavigator = new PathPageNavigator(); + + this.system.registerSyscalls( + dbSyscalls, + editorSyscalls(this), + spaceSyscalls(this), + indexerSyscalls(this.space) + ); } async init() { @@ -128,6 +132,39 @@ export class Editor implements AppEventDispatcher { pages: pages, }); }, + loadSystem: (systemJSON) => { + safeRun(async () => { + console.log("Received SYSTEM", systemJSON); + await this.system.replaceAllFromJSON(systemJSON, () => + createIFrameSandbox(this.system) + ); + console.log("Loaded plugs, now updating editor comands"); + this.editorCommands = new Map(); + for (let plug of this.system.loadedPlugs.values()) { + this.buildCommands(plug); + } + this.viewDispatch({ + type: "update-commands", + commands: this.editorCommands, + }); + }); + }, + plugUpdated: (plugName, plug) => { + safeRun(async () => { + console.log("Plug updated", plugName); + await this.system.load( + plugName, + plug, + createIFrameSandbox(this.system) + ); + }); + }, + plugRemoved: (plugName) => { + safeRun(async () => { + console.log("Plug removed", plugName); + await this.system.unload(plugName); + }); + }, }); if (this.pageNavigator.getCurrentPage() === "") { @@ -154,32 +191,16 @@ export class Editor implements AppEventDispatcher { } async loadPlugs() { - const system = new System(); + const system = new System(); system.registerSyscalls( dbSyscalls, editorSyscalls(this), spaceSyscalls(this), indexerSyscalls(this.space) ); - - console.log("Now loading core plug"); - let mainPlug = await system.load( - "core", - coreManifest, - createIFrameSandbox(system) - ); - this.plugs.push(mainPlug); - this.editorCommands = new Map(); - for (let plug of this.plugs) { - this.buildCommands(plug); - } - this.viewDispatch({ - type: "update-commands", - commands: this.editorCommands, - }); } - private buildCommands(plug: Plug) { + private buildCommands(plug: Plug) { const cmds = plug.manifest!.hooks.commands; for (let name in cmds) { let cmd = cmds[name]; @@ -192,18 +213,8 @@ export class Editor implements AppEventDispatcher { } } - // TODO: Parallelize? async dispatchAppEvent(name: AppEvent, data?: any): Promise { - let results: any[] = []; - for (let plug of this.plugs) { - let plugResults = await plug.dispatchEvent(name, data); - if (plugResults) { - for (let result of plugResults) { - results.push(result); - } - } - } - return results; + return this.system.dispatchEvent(name, data); } get currentPage(): string | undefined { @@ -338,6 +349,7 @@ export class Editor implements AppEventDispatcher { async plugCompleter(): Promise { let allCompletionResults = await this.dispatchAppEvent("editor:complete"); + console.log("Completion results", allCompletionResults); if (allCompletionResults.length === 1) { return allCompletionResults[0]; } else if (allCompletionResults.length > 1) { diff --git a/webapp/space.ts b/webapp/space.ts index b3e79d4..408c89b 100644 --- a/webapp/space.ts +++ b/webapp/space.ts @@ -5,7 +5,9 @@ import { ChangeSet, Text, Transaction } from "@codemirror/state"; import { CollabDocument, CollabEvents } from "./collab"; import { cursorEffect } from "./cursorEffect"; -import { EventEmitter } from "./event"; +import { EventEmitter } from "../common/event"; +import { SystemJSON } from "../plugbox/runtime"; +import { Manifest } from "../common/manifest"; export type SpaceEvents = { connect: () => void; @@ -13,6 +15,9 @@ export type SpaceEvents = { pageChanged: (meta: PageMeta) => void; pageDeleted: (name: string) => void; pageListUpdated: (pages: Set) => void; + loadSystem: (systemJSON: SystemJSON) => void; + plugUpdated: (plugName: string, plug: Manifest) => void; + plugRemoved: (plugName: string) => void; } & CollabEvents; export type KV = { @@ -35,6 +40,9 @@ export class Space extends EventEmitter { "pageCreated", "pageChanged", "pageDeleted", + "loadSystem", + "plugUpdated", + "plugRemoved", ].forEach((eventName) => { socket.on(eventName, (...args) => { this.emit(eventName as keyof SpaceEvents, ...args); @@ -54,7 +62,7 @@ export class Space extends EventEmitter { break; } } - if(!found) { + if (!found) { this.allPages.add(meta); console.log("New page created", meta); this.emit("pageListUpdated", this.allPages); diff --git a/webapp/types.ts b/webapp/types.ts index afb89b2..467babc 100644 --- a/webapp/types.ts +++ b/webapp/types.ts @@ -1,12 +1,4 @@ -import * as plugbox from "../plugbox/types"; - -export type NuggetHook = { - commands: { - [key: string]: CommandDef; - }; -}; - -export type Manifest = plugbox.Manifest; +import { CommandDef } from "../common/manifest"; export type PageMeta = { name: string; @@ -22,19 +14,6 @@ export type AppCommand = { export const slashCommandRegexp = /\/[\w\-]*/; -export interface CommandDef { - // Function name to invoke - invoke: string; - - // Bind to keyboard shortcut - key?: string; - mac?: string; - - // If to show in slash invoked menu and if so, with what label - // should match slashCommandRegexp - slashCommand?: string; -} - export type Notification = { id: number; message: string; diff --git a/yarn.lock b/yarn.lock index 4134113..3bdde9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1553,6 +1553,11 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/cors@^2.8.12": version "2.8.12" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" @@ -1621,6 +1626,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/node-cron@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.1.tgz#e01a874d4c2aa1a02ebc64cfd1cd8ebdbad7a996" + integrity sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg== + "@types/node@*", "@types/node@>=10.0.0", "@types/node@^17.0.21": version "17.0.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" @@ -1685,6 +1695,21 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.11": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.11.tgz#2e70f69f220bc77b4f660d72c2e1a4231f44a77d" + integrity sha512-uci4Esokrw9qGb9bvhhSVEjd6rkny/dk5PK/Qz4yxKiyppEI+dOPlNrZBahE3i+PoKFYyDxChVXZ/ysS/nrm1Q== + dependencies: + "@types/superagent" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -1838,6 +1863,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -2395,7 +2425,7 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -component-emitter@~1.3.0: +component-emitter@^1.3.0, component-emitter@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2451,6 +2481,11 @@ cookie@0.4.2, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookiejar@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2672,7 +2707,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2792,6 +2827,14 @@ dexie@^3.2.1: resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753" integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -3287,6 +3330,11 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -3341,6 +3389,25 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3577,6 +3644,11 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4666,7 +4738,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -4704,7 +4776,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4: +mime@^2.4.4, mime@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -4751,6 +4823,18 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +moment-timezone@^0.5.31: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4823,6 +4907,13 @@ node-addon-api@^4.2.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -4955,7 +5046,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -5538,11 +5629,23 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@6.9.7: version "6.9.7" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== +qs@^6.10.1: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + querystring-es3@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6120,6 +6223,31 @@ stylehacks@^*: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" +superagent@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.1.tgz#2ab187d38c3078c31c3771c0b751f10163a27136" + integrity sha512-CQ2weSS6M+doIwwYFoMatklhRbx6sVNdB99OEJ5czcP3cng76Ljqus694knFWgOj3RkrtxZqIgpe6vhe0J7QWQ== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.3" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "^2.5.0" + qs "^6.10.1" + readable-stream "^3.6.0" + semver "^7.3.5" + +supertest@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.2.tgz#04a5998fd3efaff187cb69f07a169755d655b001" + integrity sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg== + dependencies: + methods "^1.1.2" + superagent "^7.1.0" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"