Compare commits

..

No commits in common. "da7f75820fe622ca95802fd4c783b2397a258493" and "51bc9ccb327fd4faa69d693941772b61bb48b144" have entirely different histories.

4 changed files with 282 additions and 216 deletions

View File

@ -12,26 +12,12 @@
"@lumeweb/interface-relay": "git+https://git.lumeweb.com/LumeWeb/interface-relay.git", "@lumeweb/interface-relay": "git+https://git.lumeweb.com/LumeWeb/interface-relay.git",
"@lumeweb/kernel-swarm-client": "git+https://git.lumeweb.com/LumeWeb/kernel-swarm-client.git", "@lumeweb/kernel-swarm-client": "git+https://git.lumeweb.com/LumeWeb/kernel-swarm-client.git",
"@lumeweb/rpc-client": "git+https://git.lumeweb.com/LumeWeb/rpc-client.git", "@lumeweb/rpc-client": "git+https://git.lumeweb.com/LumeWeb/rpc-client.git",
"b4a": "^1.6.3",
"libkmodule": "^0.2.53" "libkmodule": "^0.2.53"
}, },
"browser": { "browser": {
"timers": "timers-browserify" "timers": "timers-browserify"
}, },
"devDependencies": { "devDependencies": {
"@scure/bip39": "^1.1.1", "@types/node": "^18.15.3"
"@skynetlabs/skynet-nodejs": "^2.9.0",
"@types/node": "^18.15.3",
"@types/read": "^0.0.29",
"cli-progress": "^3.12.0",
"esbuild": "^0.17.12",
"read": "^2.0.0",
"timers-browserify": "^2.0.12",
"typescript": "^5.0.2"
},
"pnpm": {
"patchedDependencies": {
"b4a@1.6.3": "patches/b4a@1.6.3.patch"
}
} }
} }

View File

@ -1,36 +0,0 @@
diff --git a/browser.js b/browser.js
index e07f78d17b7b4a2963e4f0062047cc96e6025f9e..a9f266a2644f3f8ebd0f6684b3e11a7519059851 100644
--- a/browser.js
+++ b/browser.js
@@ -3,6 +3,7 @@ const base64 = require('./lib/base64')
const hex = require('./lib/hex')
const utf8 = require('./lib/utf8')
const utf16le = require('./lib/utf16le')
+const Buffer = require('buffer').Buffer
const LE = new Uint8Array(Uint16Array.of(0xff).buffer)[0] === 0xff
diff --git a/index.js b/index.js
index a751d6b4d27c7dd903efbfdc87a98f61044a62b8..8bd32640afdcfbc475168d458c51f8b9162d1656 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,5 @@
+const Buffer = require('buffer').Buffer
+
function isBuffer (value) {
return Buffer.isBuffer(value) || value instanceof Uint8Array
}
diff --git a/package.json b/package.json
index eb0f792709b27e66f26658054781b4099507b5ed..69c4ff39c48b0bdd5f0bc7ebbade517e7c5d76e4 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,6 @@
"index.js",
"lib"
],
- "browser": {
- "./index.js": "./browser.js"
- },
"scripts": {
"test": "standard && brittle test/*.mjs"
},

View File

@ -1,50 +1,100 @@
// This is the standard build script for a kernel module. // This is the standard build script for a kernel module.
import * as fs from "fs"; import * as fs from "fs"
import read from "read"; import {
import * as bip39 from "@scure/bip39"; addContextToErr,
import { wordlist } from "@scure/bip39/wordlists/english.js"; b64ToBuf,
//@ts-ignore bufToHex,
import { SkynetClient } from "@skynetlabs/skynet-nodejs"; deriveRegistryEntryID,
entryIDToSkylink,
generateSeedPhraseDeterministic,
seedPhraseToSeed,
sha512,
taggedRegistryEntryKeys,
} from "libskynet"
import { generateSeedPhraseRandom, overwriteRegistryEntry, upload } from "libskynetnode"
import read from "read"
// Helper variables to make it easier to return empty values alongside errors. // Helper variables to make it easier to return empty values alongside errors.
const nu8 = new Uint8Array(0); const nu8 = new Uint8Array(0)
const nkp = { const nkp = {
publicKey: nu8, publicKey: nu8,
secretKey: nu8, secretKey: nu8,
}; }
// readFile is a wrapper for fs.readFileSync that handles the try-catch for the // readFile is a wrapper for fs.readFileSync that handles the try-catch for the
// caller. // caller.
function readFile(fileName: string): [string, string | null] { function readFile(fileName: string): [string, string | null] {
try { try {
let data = fs.readFileSync(fileName, "utf8"); let data = fs.readFileSync(fileName, "utf8")
return [data, null]; return [data, null]
} catch (err) { } catch (err) {
return ["", "unable to read file: " + JSON.stringify(err)]; return ["", "unable to read file: " + JSON.stringify(err)]
} }
} }
// readFileBinary is a wrapper for fs.readFileSync that handles the try-catch // readFileBinary is a wrapper for fs.readFileSync that handles the try-catch
// for the caller. // for the caller.
function readFileBinary(fileName: string): [Uint8Array, string | null] { function readFileBinary(fileName: string): [Uint8Array, string | null] {
try { try {
let data = fs.readFileSync(fileName, null); let data = fs.readFileSync(fileName, null)
return [data, null]; return [data, null]
} catch (err) { } catch (err) {
return [nu8, "unable to read file: " + JSON.stringify(err)]; return [nu8, "unable to read file: " + JSON.stringify(err)]
} }
} }
// writeFile is a wrapper for fs.writeFileSync which handles the try-catch in a // writeFile is a wrapper for fs.writeFileSync which handles the try-catch in a
// non-exception way. // non-exception way.
function writeFile(fileName: string, fileData: string): string | null { function writeFile(fileName: string, fileData: string): string | null {
try { try {
fs.writeFileSync(fileName, fileData); fs.writeFileSync(fileName, fileData)
return null; return null
} catch (err) { } catch (err) {
return "unable to write file: " + JSON.stringify(err); return "unable to write file: " + JSON.stringify(err)
} }
}
// hardenedSeedPhrase will take a password, harden it with 100,000 iterations
// of hashing, and then turn it into a seed phrase.
function hardenedSeedPhrase(password: string): [string, string | null] {
let pw = password
// Add some hashing iterations to the password to make it stronger.
for (let i = 0; i < 1000000; i++) {
let passU8 = new TextEncoder().encode(password)
let hashIter = sha512(passU8)
password = bufToHex(hashIter)
}
return generateSeedPhraseDeterministic(password)
}
// seedPhraseToRegistryKeys will convert a seed phrase to the set of registry
// keys that govern the registry entry where the module is published.
function seedPhraseToRegistryKeys(seedPhrase: string): [any, Uint8Array, string | null] {
let [seed, errVSP] = seedPhraseToSeed(seedPhrase)
if (errVSP !== null) {
return [nkp, nu8, addContextToErr(errVSP, "unable to compute seed phrase")]
}
let [keypair, datakey, errTREK] = taggedRegistryEntryKeys(seed, "module-build", "module-key")
if (errTREK !== null) {
return [nkp, nu8, addContextToErr(errTREK, "unable to compute registry entry keys")]
}
return [keypair, datakey, null]
}
// seedPhraseToRegistryLink will take a seedPhrase as input and convert it to
// the registry link for the module.
function seedPhraseToRegistryLink(seedPhrase: string): [string, string | null] {
let [keypair, datakey, errSPTRK] = seedPhraseToRegistryKeys(seedPhrase)
if (errSPTRK !== null) {
return ["", addContextToErr(errSPTRK, "unable to compute registry keys")]
}
let [entryID, errDREID] = deriveRegistryEntryID(keypair.publicKey, datakey)
if (errDREID !== null) {
return ["", addContextToErr(errDREID, "unable to compute registry entry id")]
}
let registryLink = entryIDToSkylink(entryID)
return [registryLink, null]
} }
// handlePass handles all portions of the script that occur after the password // handlePass handles all portions of the script that occur after the password
@ -53,166 +103,238 @@ function writeFile(fileName: string, fileData: string): string | null {
// password reader is async and we can only access the password when using a // password reader is async and we can only access the password when using a
// callback. // callback.
function handlePass(password: string) { function handlePass(password: string) {
try { try {
// If we are running prod and the seed file does not exist, we // 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 // need to confirm the password and also warn the user to use a
// secure password. // secure password.
if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { if (!fs.existsSync(seedFile) && process.argv[2] === "prod") {
// The file does not exist, we need to confirm the // The file does not exist, we need to confirm the
// password. // password.
console.log(); console.log()
console.log( console.log("No production entry found for module. Creating new production module...")
"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( console.log()
"If someone can guess the password, they can push arbitrary changes to your module." read({ prompt: "Confirm Password: ", silent: true }, function (err: any, confirmPassword: string) {
); if (err) {
console.log("Please use a secure password."); console.error("unable to fetch password:", err)
console.log(); process.exit(1)
read( }
{ prompt: "Confirm Password: ", silent: true }, if (password !== confirmPassword) {
function (err: any, confirmPassword: string) { console.error("passwords do not match")
if (err) { process.exit(1)
console.error("unable to fetch password:", err); }
process.exit(1); password = password + moduleSalt
} handlePassConfirm(password)
if (password !== confirmPassword) { })
console.error("passwords do not match"); } else {
process.exit(1); // If the seed file does exist, or if we are using dev,
} // there's no need to confirm the password but we do
handlePassConfirm(moduleSalt, password); // need to pass the logic off to the handlePassConfirm
} // callback.
); password = password + moduleSalt
} else { handlePassConfirm(password)
// If the seed file does exist, or if we are using dev, }
// there's no need to confirm the password but we do } catch (err) {
// need to pass the logic off to the handlePassConfirm console.error("Unable to read seedFile:", err)
// callback. process.exit(1)
handlePassConfirm(moduleSalt, password); }
}
} catch (err) {
console.error("Unable to read seedFile:", err);
process.exit(1);
}
} }
// handlePassConfirm handles the full script after the confirmation password // handlePassConfirm handles the full script after the confirmation password
// has been provided. If not confirmation password is needed, this function // has been provided. If not confirmation password is needed, this function
// will be called anyway using the unconfirmed password as input. // will be called anyway using the unconfirmed password as input.
function handlePassConfirm(seed: string, password: string) { function handlePassConfirm(password: string) {
// Create the seedFile if it does not exist. For dev we just save the // 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 // 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 // security sensitive. Also the dev seed does not get pushed to the
// github repo. // github repo.
// //
// For prod, we use the seed to create a new seed (called the shield) // 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 // which allows us to verify that the developer has provided the right
// password when deploying the module. The shield does get pushed to // password when deploying the module. The shield does get pushed to
// the github repo so that the production module is the same on all // the github repo so that the production module is the same on all
// devices. // devices.
if (!fs.existsSync(seedFile) && process.argv[2] !== "prod") { if (!fs.existsSync(seedFile) && process.argv[2] !== "prod") {
// Generate the seed phrase and write it to the file. // Generate the seed phrase and write it to the file.
let seedPhrase = bip39.generateMnemonic(wordlist); let [seedPhrase, errGSP] = generateSeedPhraseRandom()
let errWF = writeFile(seedFile, seedPhrase); if (errGSP !== null) {
if (errWF !== null) { console.error("Unable to generate seed phrase:", errGSP)
console.error("unable to write file:", errWF); process.exit(1)
process.exit(1); }
} let errWF = writeFile(seedFile, seedPhrase)
} else if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { if (errWF !== null) {
// Generate the seed phrase. console.error("unable to write file:", errWF)
let seedPhrase = bip39.generateMnemonic(wordlist); process.exit(1)
// Write the registry link to the file. }
} } else if (!fs.existsSync(seedFile) && process.argv[2] === "prod") {
// Generate the seed phrase.
let [seedPhrase, errGSP] = hardenedSeedPhrase(password)
if (errGSP !== null) {
console.error("Unable to generate seed phrase:", errGSP)
process.exit(1)
}
let [registryLink, errSPTRL] = seedPhraseToRegistryLink(seedPhrase)
if (errSPTRL !== null) {
console.error("Unable to generate registry link:", errSPTRL)
process.exit(1)
}
// Load or verify the seed. If this is prod, the password is used to // Write the registry link to the file.
// create and verify the seed. If this is dev, we just load the seed let errWF = writeFile(seedFile, registryLink)
// with no password. if (errWF !== null) {
let seedPhrase: string; console.error("unable to write registry link file:", errWF)
let registryLink: string; process.exit(1)
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 = { // Load or verify the seed. If this is prod, the password is used to
Filename: "index.js", // create and verify the seed. If this is dev, we just load the seed
}; // with no password.
const client = new SkynetClient("https://web3portal.com"); let seedPhrase: string
client let registryLink: string
.uploadFile("dist/index.js") if (process.argv[2] === "prod") {
.then((result: any) => { // Generate the seed phrase from the password.
console.log("Immutable Link for module:", result); let [sp, errGSP] = hardenedSeedPhrase(password)
}) if (errGSP !== null) {
.catch((err: any) => { console.error("Unable to generate seed phrase: ", errGSP)
console.error("unable to upload file", err); process.exit(1)
process.exit(1); }
}); let [rl, errSPTRL] = seedPhraseToRegistryLink(sp)
registryLink = rl
if (errSPTRL !== null) {
console.error("Unable to generate registry link:", errSPTRL)
process.exit(1)
}
let [registryLinkVerify, errRF] = readFile(seedFile)
if (errRF !== null) {
console.error("unable to read seedFile")
process.exit(1)
}
registryLinkVerify = registryLinkVerify.replace(/\n$/, "")
if (registryLink !== registryLinkVerify) {
console.error("Incorrect password")
process.exit(1)
}
seedPhrase = sp
} else {
let [sp, errRF] = readFile(seedFile)
if (errRF !== null) {
console.error("unable to read seed phrase for dev command from disk")
process.exit(1)
}
let [rl, errSPTRL] = seedPhraseToRegistryLink(sp)
registryLink = rl
if (errSPTRL !== null) {
console.error("Unable to generate registry link:", errSPTRL)
process.exit(1)
}
// Write the registry link to the module skylink dev file.
let errWF = writeFile("build/module-skylink-dev", registryLink)
if (errWF !== null) {
console.error("unable to write registry link file:", errWF)
process.exit(1)
}
seedPhrase = sp
}
// Upload the module to Skynet.
let [distFile, errRF] = readFileBinary("dist/index.js")
if (errRF !== null) {
console.error("unable to read dist file for module")
process.exit(1)
}
let metadata = {
Filename: "index.js",
}
console.log("Uploading module...")
upload(distFile, metadata)
.then((result) => {
console.log("Updating module's registry entry...")
// Update the v2 skylink.
let [keypair, datakey, errSPTRK] = seedPhraseToRegistryKeys(seedPhrase)
if (errSPTRK !== null) {
return ["", addContextToErr(errSPTRK, "unable to compute registry keys")]
}
let [bufLink, errBTB] = b64ToBuf(result)
if (errBTB !== null) {
return ["", addContextToErr(errBTB, "unable to decode skylink")]
}
overwriteRegistryEntry(keypair, datakey, bufLink)
.then(() => {
console.log("registry entry is updated")
console.log("Immutable Link for Module:", result)
console.log("Resolver Link for Module:", registryLink)
})
.catch((err: any) => {
console.log("unable to update registry entry:", err)
})
})
.catch((err) => {
console.error("unable to upload file", err)
process.exit(1)
})
} }
// Add a newline for readability. // Add a newline for readability.
console.log(); console.log()
// Check for a 'dev' or 'prod' input to the script. // Check for a 'dev' or 'prod' input to the script.
if (process.argv.length !== 3) { if (process.argv.length !== 3) {
console.error("need to provide either 'dev' or 'prod' as an input"); console.error("need to provide either 'dev' or 'prod' as an input")
process.exit(1); process.exit(1)
} }
// Create the build folder if it does not exist. // Create the build folder if it does not exist.
if (!fs.existsSync("build")) { if (!fs.existsSync("build")) {
fs.mkdirSync("build"); fs.mkdirSync("build")
} }
// Determine the seed file. // Determine the seed file.
let seedFile: string; let seedFile: string
if (process.argv[2] === "prod") { if (process.argv[2] === "prod") {
seedFile = "module-skylink"; seedFile = "module-skylink"
} else if (process.argv[2] === "dev") { } else if (process.argv[2] === "dev") {
seedFile = "build/dev-seed"; seedFile = "build/dev-seed"
} else { } else {
console.error("need to provide either 'dev' or 'prod' as an input"); console.error("need to provide either 'dev' or 'prod' as an input")
process.exit(1); process.exit(1)
} }
// If doing a prod deployment, check whether the salt file exists. If it does // If doing a prod deployment, check whether the salt file exists. If it does
// not, create it. // not, create it.
let moduleSalt: string; let moduleSalt: string
if (!fs.existsSync(".module-salt")) { if (!fs.existsSync(".module-salt")) {
moduleSalt = bip39.generateMnemonic(wordlist); let [ms, errGSPR] = generateSeedPhraseRandom()
let errWF = writeFile(".module-salt", moduleSalt); if (errGSPR !== null) {
if (errWF !== null) { console.error("unable to generate module salt:", errGSPR)
console.error("unable to write module salt file:", errWF); process.exit(1)
process.exit(1); }
} moduleSalt = ms
let errWF = writeFile(".module-salt", moduleSalt)
if (errWF !== null) {
console.error("unable to write module salt file:", errWF)
process.exit(1)
}
} else { } else {
let [ms, errRF] = readFile(".module-salt"); let [ms, errRF] = readFile(".module-salt")
if (errRF !== null) { if (errRF !== null) {
console.error("unable to read moduleSalt"); console.error("unable to read moduleSalt")
process.exit(1); process.exit(1)
} }
ms = ms.replace(/\n$/, ""); ms = ms.replace(/\n$/, "")
moduleSalt = ms; moduleSalt = ms
} }
// Need to get a password if this is a prod build. // Need to get a password if this is a prod build.
if (process.argv[2] === "prod") { if (process.argv[2] === "prod") {
read( read({ prompt: "Password: ", silent: true }, function (err: any, password: string) {
{ prompt: "Password: ", silent: true }, if (err) {
function (err: any, password: string) { console.error("unable to fetch password:", err)
if (err) { process.exit(1)
console.error("unable to fetch password:", err); }
process.exit(1); handlePass(password)
} })
handlePass(password);
}
);
} else { } else {
handlePass(""); handlePass("")
} }

View File

@ -1,6 +1,6 @@
import { addHandler, handleMessage } from "libkmodule"; import { addHandler, handleMessage } from "libkmodule";
import type { ActiveQuery } from "libkmodule"; import type { ActiveQuery } from "libkmodule";
import { createClient, SwarmClient } from "@lumeweb/kernel-swarm-client"; import { SwarmClient } from "@lumeweb/kernel-swarm-client";
import { RpcNetwork, RpcQueryOptions } from "@lumeweb/rpc-client"; import { RpcNetwork, RpcQueryOptions } from "@lumeweb/rpc-client";
import type { RPCRequest, RPCResponse } from "@lumeweb/interface-relay"; import type { RPCRequest, RPCResponse } from "@lumeweb/interface-relay";
@ -82,19 +82,13 @@ async function handleSimpleQuery(aq: ActiveQuery) {
} }
async function handleReady(aq: ActiveQuery) { async function handleReady(aq: ActiveQuery) {
const network = await getNetwork(aq);
const swarm: SwarmClient = network.swarm;
await swarm.init();
await swarm.ready();
await ( await (
await getNetwork(aq) await getNetwork(aq)
).readyWithRelays; ).readyWithRelays;
aq.respond(); aq.respond();
} }
async function createNetwork(def = true): Promise<number> { async function createNetwork(def = true): Promise<number> {
const dhtInstance = new RpcNetwork(createClient(def)); const dhtInstance = new RpcNetwork(new SwarmClient(def));
const id = nextId(); const id = nextId();
networkInstances.set(id, dhtInstance); networkInstances.set(id, dhtInstance);
@ -102,7 +96,7 @@ async function createNetwork(def = true): Promise<number> {
} }
async function getNetwork(aq: ActiveQuery): Promise<RpcNetwork> { async function getNetwork(aq: ActiveQuery): Promise<RpcNetwork> {
const { network = null } = aq?.callerInput ?? {}; const { network = null } = aq.callerInput;
await moduleReady; await moduleReady;