From 2f063e722a76f0bd2da22a3a206f0a18d8efc65e Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 18 Feb 2023 10:40:05 -0500 Subject: [PATCH] *Initial version --- build.js | 14 +++ package.json | 54 +++++++++++ rollup.config.js | 37 ++++++++ src-build/build.ts | 218 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 59 ++++++++++++ tsconfig.build.json | 14 +++ tsconfig.json | 12 +++ 7 files changed, 408 insertions(+) create mode 100644 build.js create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 src-build/build.ts create mode 100644 src/index.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/build.js b/build.js new file mode 100644 index 0000000..8424e4b --- /dev/null +++ b/build.js @@ -0,0 +1,14 @@ +import esbuild from "esbuild"; + +esbuild.buildSync({ + entryPoints: ["src/index.ts"], + outfile: "dist/index.js", + format: "iife", + bundle: true, + legalComments: "external", + // minify: true + define: { + global: "self", + }, + external: ['dns'] +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ddd5217 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "@lumeweb/kernel-handshake-node", + "types": "module", + "author": { + "name": "Hammer Technologies LLC", + "email": "contact@lumeweb.com" + }, + "scripts": { + "test": "jest", + "format": "prettier -w src", + "build-script": "tsc --project tsconfig.build.json && mv dist-build/build.js dist-build/build.mjs", + "compile": "npm run build-script && tsc && rollup -c rollup.config.js", + "build": "npm run compile && node ./dist-build/build.mjs dev" + }, + "type": "module", + "dependencies": { + "@lumeweb/hypercore-proxy-handshake": "git+https://git.lumeweb.com/LumeWeb/hypercore-proxy-handshake.git", + "@lumeweb/kernel-swarm-client": "link:/home/derrick/Projects/lume/kernel/kernel-swarm-client", + "libkmodule": "^0.2.53" + }, + "devDependencies": { + "@rollup/plugin-alias": "^4.0.3", + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-inject": "^5.0.3", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^11.0.0", + "@screamingvoid/sodium-universal": "^0.1.1", + "@scure/bip39": "^1.1.1", + "@skynetlabs/skynet-nodejs": "^2.9.0", + "@types/node": "^18.14.0", + "@types/read": "^0.0.29", + "@types/streamx": "^2.9.1", + "cli-progress": "^3.11.2", + "esbuild": "^0.17.8", + "eventemitter3": "^5.0.0", + "node-stdlib-browser": "^1.2.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "prettier": "^2.8.4", + "read": "^2.0.0", + "rollup": "^3.17.0", + "rollup-plugin-polyfill": "^4.0.0", + "rollup-plugin-polyfill-inject": "^1.0.4", + "rollup-plugin-polyfill-node": "^0.12.0", + "setimmediate": "^1.0.5", + "tslib": "^2.5.0", + "typescript": "^4.9.5", + "util": "^0.12.5", + "vite": "^4.1.2", + "vite-plugin-node-polyfills": "^0.7.0", + "vite-plugin-node-stdlib-browser": "^0.1.1" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..d0486c8 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,37 @@ +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import stdLibBrowser from "node-stdlib-browser"; +import { handleCircularDependancyWarning } from "node-stdlib-browser/helpers/rollup/plugin"; +import alias from "@rollup/plugin-alias"; +import inject from "@rollup/plugin-inject"; +import { defineConfig } from "rollup"; +import * as fs from "fs"; +import path from "path"; + +export default defineConfig({ + input: "build/index.js", + output: { + file: "dist/index.js", + format: "iife", + inlineDynamicImports: true, + banner: fs.readFileSync( + path.resolve("node_modules/setimmediate/setImmediate.js") + ), + }, + plugins: [ + json(), + alias({ + entries: stdLibBrowser, + }), + resolve({ browser: true, preferBuiltins: true, dedupe: ["libkmodule"] }), + commonjs({ transformMixedEsModules: true }), + inject({ + process: stdLibBrowser.process, + Buffer: [stdLibBrowser.buffer, "Buffer"], + }), + ], + onwarn: (warning, rollupWarn) => { + handleCircularDependancyWarning(warning, rollupWarn); + }, +}); diff --git a/src-build/build.ts b/src-build/build.ts new file mode 100644 index 0000000..8703c34 --- /dev/null +++ b/src-build/build.ts @@ -0,0 +1,218 @@ +// This is the standard build script for a kernel module. + +import * as fs from "fs"; +import read from "read"; +import * as bip39 from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english.js"; +//@ts-ignore +import { SkynetClient } from "@skynetlabs/skynet-nodejs"; + +// Helper variables to make it easier to return empty values alongside errors. +const nu8 = new Uint8Array(0); +const nkp = { + publicKey: nu8, + secretKey: nu8, +}; + +// readFile is a wrapper for fs.readFileSync that handles the try-catch for the +// caller. +function readFile(fileName: string): [string, string | null] { + try { + let data = fs.readFileSync(fileName, "utf8"); + return [data, null]; + } catch (err) { + return ["", "unable to read file: " + JSON.stringify(err)]; + } +} + +// readFileBinary is a wrapper for fs.readFileSync that handles the try-catch +// for the caller. +function readFileBinary(fileName: string): [Uint8Array, string | null] { + try { + let data = fs.readFileSync(fileName, null); + return [data, null]; + } catch (err) { + return [nu8, "unable to read file: " + JSON.stringify(err)]; + } +} + +// writeFile is a wrapper for fs.writeFileSync which handles the try-catch in a +// non-exception way. +function writeFile(fileName: string, fileData: string): string | null { + try { + fs.writeFileSync(fileName, fileData); + return null; + } catch (err) { + return "unable to write file: " + JSON.stringify(err); + } +} + +// handlePass handles all portions of the script that occur after the password +// has been requested. If no password needs to be requested, handlePass will be +// called with a null input. We need to structure the code this way because the +// password reader is async and we can only access the password when using a +// callback. +function handlePass(password: string) { + try { + // If we are running prod and the seed file does not exist, we + // need to confirm the password and also warn the user to use a + // secure password. + if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { + // The file does not exist, we need to confirm the + // password. + console.log(); + console.log( + "No production entry found for module. Creating new production module..." + ); + console.log( + "If someone can guess the password, they can push arbitrary changes to your module." + ); + console.log("Please use a secure password."); + console.log(); + read( + { prompt: "Confirm Password: ", silent: true }, + function (err: any, confirmPassword: string) { + if (err) { + console.error("unable to fetch password:", err); + process.exit(1); + } + if (password !== confirmPassword) { + console.error("passwords do not match"); + process.exit(1); + } + handlePassConfirm(moduleSalt, password); + } + ); + } else { + // If the seed file does exist, or if we are using dev, + // there's no need to confirm the password but we do + // need to pass the logic off to the handlePassConfirm + // callback. + handlePassConfirm(moduleSalt, password); + } + } catch (err) { + console.error("Unable to read seedFile:", err); + process.exit(1); + } +} + +// handlePassConfirm handles the full script after the confirmation password +// has been provided. If not confirmation password is needed, this function +// will be called anyway using the unconfirmed password as input. +function handlePassConfirm(seed: string, password: string) { + // Create the seedFile if it does not exist. For dev we just save the + // seed to disk outright, because this is a dev build and therefore not + // security sensitive. Also the dev seed does not get pushed to the + // github repo. + // + // For prod, we use the seed to create a new seed (called the shield) + // which allows us to verify that the developer has provided the right + // password when deploying the module. The shield does get pushed to + // the github repo so that the production module is the same on all + // devices. + if (!fs.existsSync(seedFile) && process.argv[2] !== "prod") { + // Generate the seed phrase and write it to the file. + let seedPhrase = bip39.generateMnemonic(wordlist); + let errWF = writeFile(seedFile, seedPhrase); + if (errWF !== null) { + console.error("unable to write file:", errWF); + process.exit(1); + } + } else if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { + // Generate the seed phrase. + let seedPhrase = bip39.generateMnemonic(wordlist); + // Write the registry link to the file. + } + + // Load or verify the seed. If this is prod, the password is used to + // create and verify the seed. If this is dev, we just load the seed + // with no password. + let seedPhrase: string; + let registryLink: string; + if (process.argv[2] === "prod") { + // Generate the seed phrase from the password. + seedPhrase = bip39.generateMnemonic(wordlist); + } else { + let [sp, errRF] = readFile(seedFile); + if (errRF !== null) { + console.error("unable to read seed phrase for dev command from disk"); + process.exit(1); + } + seedPhrase = sp; + } + + let metadata = { + Filename: "index.js", + }; + const client = new SkynetClient("https://web3portal.com"); + client + .uploadFile("dist/index.js") + .then((result: any) => { + console.log("Immutable Link for kernel:", result); + }) + .catch((err: any) => { + console.error("unable to upload file", err); + process.exit(1); + }); +} + +// Add a newline for readability. +console.log(); + +// Check for a 'dev' or 'prod' input to the script. +if (process.argv.length !== 3) { + console.error("need to provide either 'dev' or 'prod' as an input"); + process.exit(1); +} + +// Create the build folder if it does not exist. +if (!fs.existsSync("build")) { + fs.mkdirSync("build"); +} + +// Determine the seed file. +let seedFile: string; +if (process.argv[2] === "prod") { + seedFile = "module-skylink"; +} else if (process.argv[2] === "dev") { + seedFile = "build/dev-seed"; +} else { + console.error("need to provide either 'dev' or 'prod' as an input"); + process.exit(1); +} + +// If doing a prod deployment, check whether the salt file exists. If it does +// not, create it. +let moduleSalt: string; +if (!fs.existsSync(".module-salt")) { + moduleSalt = bip39.generateMnemonic(wordlist); + let errWF = writeFile(".module-salt", moduleSalt); + if (errWF !== null) { + console.error("unable to write module salt file:", errWF); + process.exit(1); + } +} else { + let [ms, errRF] = readFile(".module-salt"); + if (errRF !== null) { + console.error("unable to read moduleSalt"); + process.exit(1); + } + ms = ms.replace(/\n$/, ""); + moduleSalt = ms; +} + +// Need to get a password if this is a prod build. +if (process.argv[2] === "prod") { + read( + { prompt: "Password: ", silent: true }, + function (err: any, password: string) { + if (err) { + console.error("unable to fetch password:", err); + process.exit(1); + } + handlePass(password); + } + ); +} else { + handlePass(""); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1abc653 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,59 @@ +import type { ActiveQuery } from "libkmodule"; +import { addHandler, handleMessage } from "libkmodule"; +import { createClient } from "@lumeweb/kernel-swarm-client"; +import HandshakeProxy from "@lumeweb/hypercore-proxy-handshake"; + +const PROTOCOL = "lumeweb.proxy.handshake"; + +onmessage = handleMessage; + +let moduleLoadedResolve: Function; +let moduleLoaded: Promise = new Promise((resolve) => { + moduleLoadedResolve = resolve; +}); + +addHandler("presentSeed", handlePresentSeed); +addHandler("ready", handleReady); +addHandler("query", handleQuery); + +let swarm; +let proxy: HandshakeProxy; + +async function handlePresentSeed(aq: ActiveQuery) { + swarm = createClient(); + proxy = new HandshakeProxy({ swarm, listen: true }); + + swarm.join(PROTOCOL); + await swarm.start(); + + moduleLoadedResolve(); +} + +async function handleReady(aq: ActiveQuery) { + await moduleLoaded; + + await new Promise((resolve): void => { + if (proxy.node.chain.synced) { + return resolve(null); + } + + proxy.node.pool.on("full", () => { + resolve(null); + }); + }); + + aq.respond(); +} + +async function handleQuery(aq: ActiveQuery) { + if (!proxy.node.chain.synced) { + aq.reject("not ready"); + return; + } + + try { + aq.respond(await proxy.node.rpc.call(aq.callerInput)); + } catch (e) { + aq.reject((e as Error).message); + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..90439ba --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": true, + "outDir": "./dist-build", + "strict": true, + "esModuleInterop": true + }, + "include": ["src-build"], + "exclude": ["node_modules", "**/__tests__/*"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7baf8cf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "outDir": "./build", + "strict": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"], + "exclude": ["node_modules", "**/__tests__/*"] +}