diff --git a/README.md b/README.md
index 04a0f7f..1f2ea49 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
-# kernel-test
+# kernel-tester
A kernel module test library for Skynet
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..fe99739
--- /dev/null
+++ b/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@lumeweb/kernel-tester",
+ "version": "0.1.0",
+ "scripts": {
+ "build": "tsc && rollup -c && cp dist/tester.js public/tester.js && cp build/*.d.ts dist && rm -f dist/tester.js && rm -rf build/"
+ },
+ "main": "dist/index.js",
+ "types": "types",
+ "dependencies": {
+ "libkernel": "^0.1.41",
+ "libskynet": "^0.0.48",
+ "libskynetnode": "^0.1.3",
+ "static-server": "^2.2.1"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^22.0.1",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^13.3.0",
+ "@types/node": "^18.0.0",
+ "@types/ws": "^8.5.3",
+ "prettier": "^2.7.1",
+ "puppeteer": "^15.2.0",
+ "rollup": "^2.75.7",
+ "rollup-plugin-polyfill-node": "^0.9.0",
+ "typescript": "^4.5"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..9ee7e33
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Tester
+
+
+
+
+
+
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..6a4d8ec
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,25 @@
+import resolve from "@rollup/plugin-node-resolve";
+import commonjs from "@rollup/plugin-commonjs";
+import json from "@rollup/plugin-json";
+
+export default [
+ {
+ input: "build/tester.js",
+ output: {
+ file: "dist/tester.js",
+ format: "iife",
+ },
+ plugins: [resolve(), commonjs(), json()],
+ inlineDynamicImports: true,
+ },
+ {
+ input: "build/index.js",
+ output: {
+ file: "dist/index.js",
+ format: "cjs",
+ exports: "auto",
+ },
+ plugins: [resolve({ preferBuiltins: true }), commonjs(), json()],
+ inlineDynamicImports: true,
+ },
+];
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..3d54df5
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,167 @@
+import {
+ b64ToBuf,
+ bufToHex,
+ deriveChildSeed,
+ dictionary,
+ seedPhraseToSeed,
+ taggedRegistryEntryKeys,
+} from "libskynet";
+import { SEED_BYTES, seedToChecksumWords } from "libskynet/dist/seed";
+import { DICTIONARY_UNIQUE_PREFIX } from "libskynet/dist/dictionary";
+import * as path from "path";
+import { overwriteRegistryEntry } from "libskynetnode";
+import * as kernel from "libkernel";
+import { webcrypto } from "crypto";
+// @ts-ignore
+import * as StaticServer from "static-server";
+import { Page } from "puppeteer";
+import { errTuple } from "libskynet/dist";
+
+export const KERNEL_TEST_SUITE =
+ "AQCPJ9WRzMpKQHIsPo8no3XJpUydcDCjw7VJy8lG1MCZ3g";
+export const KERNEL_HELPER_MODULE =
+ "AQCoaLP6JexdZshDDZRQaIwN3B7DqFjlY7byMikR7u1IEA";
+export const TEST_KERNEL_SKLINK =
+ "AQCw2_9rg0Fxuy8ky3pvLiDhcJTmAqthy1Buc7Frl2v2fA";
+const SEED_ENTROPY_WORDS = 13;
+const crypto = webcrypto as unknown as Crypto;
+
+export function generateSeedPhrase() {
+ // Get the random numbers for the seed phrase. Typically, you need to
+ // have code that avoids bias by checking the random results and
+ // re-rolling the random numbers if the result is outside of the range
+ // of numbers that would produce no bias. Because the search space
+ // (1024) evenly divides the random number space (2^16), we can skip
+ // this step and just use a modulus instead. The result will have no
+ // bias, but only because the search space is a power of 2.
+ let randNums = new Uint16Array(SEED_ENTROPY_WORDS);
+ crypto.getRandomValues(randNums);
+ // Generate the seed phrase from the randNums.
+ let seedWords = [];
+ for (let i = 0; i < SEED_ENTROPY_WORDS; i++) {
+ let wordIndex = randNums[i] % dictionary.length;
+ seedWords.push(dictionary[wordIndex]);
+ }
+ // Convert the seedWords to a seed.
+ let [seed] = seedWordsToSeed(seedWords);
+ // Compute the checksum.
+ let [checksumOne, checksumTwo, err2] = seedToChecksumWords(
+ seed as Uint8Array
+ );
+ // Assemble the final seed phrase and set the text field.
+ return [...seedWords, checksumOne, checksumTwo].join(" ");
+}
+
+function seedWordsToSeed(seedWords: string[]) {
+ // Input checking.
+ if (seedWords.length !== SEED_ENTROPY_WORDS) {
+ return [
+ new Uint8Array(0),
+ `Seed words should have length ${SEED_ENTROPY_WORDS} but has length ${seedWords.length}`,
+ ];
+ }
+ // We are getting 16 bytes of entropy.
+ let bytes = new Uint8Array(SEED_BYTES);
+ let curByte = 0;
+ let curBit = 0;
+ for (let i = 0; i < SEED_ENTROPY_WORDS; i++) {
+ // Determine which number corresponds to the next word.
+ let word = -1;
+ for (let j = 0; j < dictionary.length; j++) {
+ if (
+ seedWords[i].slice(0, DICTIONARY_UNIQUE_PREFIX) ===
+ dictionary[j].slice(0, DICTIONARY_UNIQUE_PREFIX)
+ ) {
+ word = j;
+ break;
+ }
+ }
+ if (word === -1) {
+ return [
+ new Uint8Array(0),
+ `word '${seedWords[i]}' at index ${i} not found in dictionary`,
+ ];
+ }
+ let wordBits = 10;
+ if (i === SEED_ENTROPY_WORDS - 1) {
+ wordBits = 8;
+ }
+ // Iterate over the bits of the 10- or 8-bit word.
+ for (let j = 0; j < wordBits; j++) {
+ let bitSet = (word & (1 << (wordBits - j - 1))) > 0;
+ if (bitSet) {
+ bytes[curByte] |= 1 << (8 - curBit - 1);
+ }
+ curBit += 1;
+ if (curBit >= 8) {
+ // Current byte has 8 bits, go to the next byte.
+ curByte += 1;
+ curBit = 0;
+ }
+ }
+ }
+ return [bytes, null];
+}
+
+export async function login(page: Page, seed = generateSeedPhrase()) {
+ await page.goto("http://skt.us");
+
+ let userSeed: Uint8Array;
+
+ [userSeed] = seedPhraseToSeed(seed);
+ let seedHex = bufToHex(userSeed);
+
+ await page.evaluate((seed: string) => {
+ window.localStorage.setItem("v1-seed", seed);
+ }, seedHex);
+
+ let kernelEntrySeed = deriveChildSeed(userSeed, "userPreferredKernel2");
+
+ // Get the registry keys.
+ let [keypair, dataKey] = taggedRegistryEntryKeys(
+ kernelEntrySeed,
+ "user kernel"
+ );
+
+ await overwriteRegistryEntry(
+ keypair,
+ dataKey,
+ b64ToBuf(TEST_KERNEL_SKLINK)[0]
+ );
+}
+
+export async function loadTester(page: Page, port = 8080) {
+ const server = new StaticServer({
+ rootPath: path.resolve(__dirname, "..", "public"),
+ port,
+ host: "localhost",
+ });
+ await new Promise((resolve) => {
+ server.start(resolve);
+ });
+ await page.goto(`http://localhost:${port}/`);
+ await page.evaluate(() => {
+ return kernel.init();
+ });
+}
+
+class Tester {
+ private page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ async callModule(id: string, method: string, data = {}): Promise {
+ return this.page.evaluate(
+ async (id, method, data) => {
+ return kernel.callModule(id, method, data);
+ },
+ id,
+ method,
+ data
+ );
+ }
+}
+
+export const tester = (page: Page) => new Tester(page);
diff --git a/src/tester.ts b/src/tester.ts
new file mode 100644
index 0000000..b424e10
--- /dev/null
+++ b/src/tester.ts
@@ -0,0 +1,7 @@
+import * as kernel from "libkernel";
+import * as skynet from "libskynet";
+
+// @ts-ignore
+global.kernel = kernel;
+// @ts-ignore
+global.skynet = skynet;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8204e30
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "commonjs",
+ "declaration": true,
+ "moduleResolution": "node",
+ "outDir": "./build",
+ "strict": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": [
+ "src",
+ ],
+ "exclude": ["node_modules", "**/__tests__/*"]
+}