Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

18 changed files with 1 additions and 21153 deletions

View File

@ -1,13 +0,0 @@
name: Build/Publish
on:
push:
branches:
- master
- develop
- develop-*
jobs:
main:
uses: lumeweb/github-node-deploy-workflow/.github/workflows/main.yml@master
secrets: inherit

View File

@ -1,8 +0,0 @@
{
"preset": [
"@lumeweb/presetter-kernel-module-preset"
],
"config": {
"browser": true
}
}

View File

@ -1,126 +0,0 @@
# [0.1.0-develop.23](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.22...v0.1.0-develop.23) (2023-11-18)
# [0.1.0-develop.22](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.21...v0.1.0-develop.22) (2023-10-15)
### Bug Fixes
* add resolvedDomain to Module type and use it instead in createModule so that the domain id stays consistent to the potential resolver version, when used as a callerDomain ([68642cc](https://git.lumeweb.com/LumeWeb/kernel/commit/68642ccb03fafb27662f51905ad628e70198b826))
# [0.1.0-develop.21](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.20...v0.1.0-develop.21) (2023-10-13)
# [0.1.0-develop.20](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.19...v0.1.0-develop.20) (2023-10-11)
# [0.1.0-develop.19](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.18...v0.1.0-develop.19) (2023-09-20)
### Features
* change workers to use a dedicated iframe that for now is hosted. This should unlock a dedicated thread per worker and prevent cpu fighting ([cfeca11](https://git.lumeweb.com/LumeWeb/kernel/commit/cfeca113007e6d3c331ebed64bfbc300cb441f0a))
# [0.1.0-develop.18](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.17...v0.1.0-develop.18) (2023-09-16)
### Bug Fixes
* need to pass sw property back with responseNonce ([265d955](https://git.lumeweb.com/LumeWeb/kernel/commit/265d95579550757c7d7e9d4a9b23f6690dbd2c29))
# [0.1.0-develop.17](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.16...v0.1.0-develop.17) (2023-09-13)
### Features
* add cached modules support with leveldb/indexeddb ([6e148c1](https://git.lumeweb.com/LumeWeb/kernel/commit/6e148c1e0a761ac1b253074c150b61d356285560))
# [0.1.0-develop.16](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.15...v0.1.0-develop.16) (2023-09-11)
### Bug Fixes
* need to use query not event ([10eb4a9](https://git.lumeweb.com/LumeWeb/kernel/commit/10eb4a990e333b1ef5bed05b0fbb2908d432569b))
# [0.1.0-develop.15](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.14...v0.1.0-develop.15) (2023-09-11)
### Bug Fixes
* pass the fullBytes of the hash, and return the CID in string form with fromRegistry ([2d61697](https://git.lumeweb.com/LumeWeb/kernel/commit/2d616979c3249c7c0d9c3da1574ce9ca22352405))
* use globalThis ([23c2848](https://git.lumeweb.com/LumeWeb/kernel/commit/23c2848683ab6ce5c8e2b46c04f0b5f5ae7d3234))
* use globalThis ([b6142a6](https://git.lumeweb.com/LumeWeb/kernel/commit/b6142a654d3040baa18bfae7caa2f4db5aa458c0))
### Features
* add service worker support ([1524ff5](https://git.lumeweb.com/LumeWeb/kernel/commit/1524ff5c57df1b7e1c88df52c6c1c0d4851dfc15))
# [0.1.0-develop.14](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.13...v0.1.0-develop.14) (2023-09-08)
# [0.1.0-develop.13](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.12...v0.1.0-develop.13) (2023-09-04)
# [0.1.0-develop.12](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.11...v0.1.0-develop.12) (2023-09-04)
# [0.1.0-develop.11](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.10...v0.1.0-develop.11) (2023-09-04)
# [0.1.0-develop.10](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.9...v0.1.0-develop.10) (2023-09-03)
# [0.1.0-develop.9](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.8...v0.1.0-develop.9) (2023-09-03)
### Bug Fixes
* finalModule needs to be moduleDomain ([6b0d43f](https://git.lumeweb.com/LumeWeb/kernel/commit/6b0d43fe9b41150a768025d8d6b2edc7033e1607))
* have resolveModuleRegistryEntry decode the cid and pass the pubkey ([e6318cd](https://git.lumeweb.com/LumeWeb/kernel/commit/e6318cdc38b839de8c95d36c1f5276596b57bd83))
# [0.1.0-develop.8](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.7...v0.1.0-develop.8) (2023-09-02)
### Bug Fixes
* missing return ([7d44208](https://git.lumeweb.com/LumeWeb/kernel/commit/7d442081322aad0bab186fa57476ab15fc07e30c))
* need to call swarm not peerDiscoveryRegistry ([27ba637](https://git.lumeweb.com/LumeWeb/kernel/commit/27ba637ac224bcac76635bc8659d76d0472f342d))
* use message.data not message.data.data ([4a73a27](https://git.lumeweb.com/LumeWeb/kernel/commit/4a73a2779d3a8020f06f0d8d75c386abf66e15b1))
# [0.1.0-develop.7](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.6...v0.1.0-develop.7) (2023-09-02)
# [0.1.0-develop.6](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.5...v0.1.0-develop.6) (2023-09-02)
# [0.1.0-develop.5](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.4...v0.1.0-develop.5) (2023-09-01)
### Bug Fixes
* fix import ([787c7c6](https://git.lumeweb.com/LumeWeb/kernel/commit/787c7c6637f8291c4b2e9558f1d68ebabcb2e225))
### Features
* add initial fallback registry support ([5cc457d](https://git.lumeweb.com/LumeWeb/kernel/commit/5cc457d78b40541c1ecfa1d6a5c07274a7d48ab6))
* add initial support to query the network for a registry entry for a module ([ba4c6dc](https://git.lumeweb.com/LumeWeb/kernel/commit/ba4c6dcb9552eeb7cf4b87e31d2f878b1eb83198))
# [0.1.0-develop.4](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.3...v0.1.0-develop.4) (2023-08-10)
# [0.1.0-develop.3](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.2...v0.1.0-develop.3) (2023-08-10)
# [0.1.0-develop.2](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.1.0-develop.1...v0.1.0-develop.2) (2023-07-20)
### Bug Fixes
* HACK, alias rootPrivateKey to rootKey temporarily ([3f5adbf](https://git.lumeweb.com/LumeWeb/kernel/commit/3f5adbfb11e2d7f505db843ee277f30482165992))
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.0.1...v0.1.0-develop.1) (2023-07-18)
### Bug Fixes
* always launch worker inside of createModule ([7093494](https://git.lumeweb.com/LumeWeb/kernel/commit/70934942275b6c53c0723b3e88421f86378310e5))
* pass root key to module ([27830c1](https://git.lumeweb.com/LumeWeb/kernel/commit/27830c139759e99006d0e99b8ddb80413344664c))
* update libweb ([e72075f](https://git.lumeweb.com/LumeWeb/kernel/commit/e72075f99aa4da9c040aa0300bc11688f81c714d))
* update libweb ([4f507fe](https://git.lumeweb.com/LumeWeb/kernel/commit/4f507fe79eeadbb237ac43a0021b983e086ddaed))
* update libweb ([b98ded6](https://git.lumeweb.com/LumeWeb/kernel/commit/b98ded63c8260d46873f27098f898da2aa7c00a1))
* update object data ([4408dae](https://git.lumeweb.com/LumeWeb/kernel/commit/4408dae8eb4f1c431cd8c8028c00d4bade680a89))
### Features
* initial version ([03f4083](https://git.lumeweb.com/LumeWeb/kernel/commit/03f4083674e143316084a765e1b6091d5bbcad52))

View File

@ -1,7 +1,6 @@
MIT License
Copyright (c) 2023 Hammer Technologies LLC
Copyright (c) 2022 Skynet Labs
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,3 +1,2 @@
# kernel
Code is based on, and copies much of its work from https://github.com/SkynetLabs/skynet-kernel/tree/beta/kernel

19734
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
{
"name": "@lumeweb/kernel",
"version": "0.1.0-develop.23",
"type": "module",
"repository": {
"type": "git",
"url": "gitea@git.lumeweb.com:LumeWeb/kernel.git"
},
"devDependencies": {
"@lumeweb/presetter-kernel-module-preset": "^0.1.0-develop.43",
"binconv": "^0.2.0",
"presetter": "*",
"semantic-release": "^21.0.5"
},
"readme": "ERROR: No README data found!",
"scripts": {
"prepare": "presetter bootstrap",
"build": "run build",
"semantic-release": "semantic-release"
},
"dependencies": {
"@lumeweb/libkernel": "0.1.0-develop.53"
}
}

View File

@ -1,36 +0,0 @@
// notableErrors is a persistent list of errors that should be checked after
// testing. You should only add to this array in the event of an error that
// indicates a bug with the kernel.
import { OpenQueryResponse } from "./queries.js";
const notableErrors: string[] = [];
// respondErr will send an error response to the caller that closes out the
// query for the provided nonce. The extra inputs of 'messagePortal' and
// 'isWorker' are necessary to handle the fact that the MessageEvent you get
// from a worker message is different from the MessageEvent you get from a
// window message, and also from the fact that postMessage has different
// arguments depending on whether the messagePortal is a worker or a window.
function respondErr(
event: MessageEvent,
messagePortal: any,
isWorker: boolean,
isInternal: false | ((message: OpenQueryResponse) => void),
err: string,
) {
const message = {
nonce: event.data.nonce,
method: "response",
data: {},
err,
} as OpenQueryResponse;
if (isWorker) {
messagePortal.postMessage(message);
} else if (isInternal) {
isInternal(message);
} else {
messagePortal.postMessage(message, event.origin);
}
}
export { notableErrors, respondErr };

View File

@ -1,43 +0,0 @@
import { logLargeObjects } from "./logLargeState.js";
import { log, logErr } from "./log.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import {
maybeInitDefaultPortals,
setActivePortalMasterKey,
} from "@lumeweb/libweb";
import { Client } from "@lumeweb/libportal";
import { addContextToErr } from "@lumeweb/libkernel";
import { handleIncomingMessage } from "./message.js";
import { activeKey } from "./key.js";
declare global {
interface Window {
bootloaderPortals: Client[];
}
}
// Kick off the thread that will periodically log all of the large objects in
// the kernel, so that it's easier to check for memory leaks.
logLargeObjects();
// Write a log that declares the kernel version and distribution.
log("init", "Lume Web Kernel v" + KERNEL_VERSION + "-" + KERNEL_DISTRO);
/*
Try to load either our saved portal(s) or the default portal(s)
*/
setActivePortalMasterKey(activeKey);
let portalLoadErr = false;
try {
maybeInitDefaultPortals();
} catch (e) {
let err = addContextToErr(e, "unable to init portals");
logErr(err);
portalLoadErr = true;
}
if (!portalLoadErr) {
window.addEventListener("message", handleIncomingMessage);
}

View File

@ -1,5 +0,0 @@
import { hexToBytes } from "@lumeweb/libweb";
export const activeKey = hexToBytes(
globalThis.localStorage.getItem("key") as string,
);

View File

@ -1,31 +0,0 @@
import { objAsString } from "@lumeweb/libkernel";
// wLog is a wrapper for the log and logErr functions, to deduplicate code.
//
// TODO: Need to implement a tag system for the logging. We will use the
// dashboard to control logging messages and verbosity.
function wLog(isErr: boolean, tag: string, ...inputs: any) {
let message = "[lumeweb-kernel]\n" + tag;
for (let i = 0; i < inputs.length; i++) {
message += "\n";
message += objAsString(inputs[i]);
}
window.parent.postMessage(
{
method: "log",
data: {
isErr,
message,
},
},
"*",
);
}
function log(tag: string, ...inputs: any) {
wLog(false, tag, ...inputs);
}
function logErr(tag: string, ...inputs: any) {
wLog(true, tag, ...inputs);
}
export { log, logErr };

View File

@ -1,28 +0,0 @@
import { notableErrors } from "./err.js";
import { log } from "./log.js";
import { modules, modulesLoading, queries } from "./queries.js";
// Set up a loop that will periodically log all of the large objects in the
// kernel, for the sake of making detection and debugging easier in the event
// of a
let waitTime = 30000;
function logLargeObjects() {
const queriesLenStr = Object.keys(queries).length.toString();
const modulesLenStr = Object.keys(modules).length.toString();
const modulesLoadingLenStr = Object.keys(modulesLoading).length.toString();
log(
"open queries :: open modules :: modules loading :: notable errors : " +
queriesLenStr +
" :: " +
modulesLenStr +
" :: " +
modulesLoadingLenStr +
" :: " +
notableErrors.length,
);
waitTime *= 1.25;
setTimeout(logLargeObjects, waitTime);
}
setTimeout(logLargeObjects, waitTime);
export { logLargeObjects };

View File

@ -1,117 +0,0 @@
// Overwrite the handleIncomingMessage function that gets called at the end of the
// event handler, allowing us to support custom messages.
import { logErr } from "./log.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import { notableErrors, respondErr } from "./err.js";
import { handleModuleCall, handleQueryUpdate } from "./queries.js";
export const handleIncomingMessage = function (event: any) {
// Ignore all messages from ourself.
if (event.source === window) {
return;
}
if (event.origin.endsWith(".module.kernel.lumeweb.com")) {
return;
}
// Input validation.
if (!("method" in event.data)) {
logErr("handleIncomingMessage", "kernel request is missing 'method' field");
return;
}
if (!("nonce" in event.data)) {
logErr(
"handleIncomingMessage",
"message sent to kernel with no nonce field",
event.data,
);
return;
}
// Establish a debugging handler that a developer can call to verify
// that round-trip communication has been correctly programmed between
// the kernel and the calling application.
//
// It was easier to inline the message than to abstract it.
if (event.data.method === "version") {
event.source.postMessage(
{
nonce: event.data.nonce,
method: "response",
err: null,
data: {
distribution: KERNEL_DISTRO,
version: KERNEL_VERSION,
},
},
event.origin,
);
return;
}
// Establish a debugging handler to return any noteworthy errors that the
// kernel has encountered. This is mainly intended to be used by the test
// suite.
if (event.data.method === "checkErrs") {
event.source.postMessage(
{
nonce: event.data.nonce,
method: "response",
err: null,
data: {
errs: notableErrors,
},
},
event.origin,
);
return;
}
// Establish handlers for the major kernel methods.
if (event.data.method === "moduleCall") {
// Check for a domain. If the message was sent by a browser
// extension, we trust the domain provided by the extension,
// otherwise we use the domain of the parent as the domain.
// This does mean that the kernel is trusting that the user has
// no malicious browser extensions, as we aren't checking for
// **which** extension is sending the message, we are only
// checking that the message is coming from a browser
// extension.
if (event.origin.startsWith("moz") && !("domain" in event.data)) {
logErr(
"moduleCall",
"caller is an extension, but no domain was provided",
);
respondErr(
event,
event.source,
false,
false,
"caller is an extension, but not domain was provided",
);
return;
}
let domain;
if (event.origin.startsWith("moz")) {
domain = event.data.domain;
} else {
domain = new URL(event.origin).hostname;
}
handleModuleCall(event, event.source, domain, false, false);
return;
}
if (event.data.method === "queryUpdate") {
handleQueryUpdate(event);
return;
}
// Unrecognized method, reject the query.
respondErr(
event,
event.source,
false,
false,
"unrecognized method: " + event.data.method,
);
};

View File

@ -1,61 +0,0 @@
import { internalModuleCall, modules } from "./queries.js";
import { CID, SignedRegistryEntry } from "@lumeweb/libs5";
import { Level } from "level";
let moduleStore: Level<string, Uint8Array>;
const CORE_MODULES = {
swarm: "zrjTDyEX8Mh2PdDdRj5YL2byFGrYe1ksczRwPaTRFaCGSMG",
peerDiscoveryRegistry: "zrjD6CEchDtSex5VHjzMNSAdkJpMNfCtbxSnftgtfvtnsdY",
ircPeerDiscovery: "zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ",
s5: "zrjLjKVByzt233rfcjWvTQXrMfGFa11oBLydPaUk7gwnC2d",
networkRegistry: "zrjTCwTcK5Vco1h7cdUQKzs6yzeqm7vC5u5Lo9y1uhTyxnv",
};
export async function networkReady() {
for (const module of [CORE_MODULES.swarm]) {
if (!moduleLoaded(module)) {
return false;
}
}
const resolvers = await internalModuleCall(CORE_MODULES.swarm, "getRelays");
return resolvers.length > 0;
}
function moduleLoaded(module: string) {
return module in modules;
}
export async function resolveModuleRegistryEntry(module: string) {
const cid = CID.decode(module);
const pubkey = cid.hash.fullBytes;
const signedEntry = (await internalModuleCall(
CORE_MODULES.s5,
"getRegistryEntry",
{ pubkey },
)) as SignedRegistryEntry;
return CID.fromRegistry(signedEntry.data).toString();
}
async function initStore() {
if (moduleStore) {
return;
}
const db = new Level<string, Uint8Array>("kernel-module-store");
await db.open();
moduleStore = db;
}
export async function store() {
if (!moduleStore) {
await initStore();
}
return moduleStore;
}

View File

@ -1,789 +0,0 @@
import { notableErrors, respondErr } from "./err.js";
import { log, logErr } from "./log.js";
import { activeKey } from "./key.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import {
addContextToErr,
bufToB64,
encodeU64,
equalBytes,
Err,
objAsString,
sha512,
} from "@lumeweb/libkernel";
import { deriveChildKey, downloadSmallObject } from "@lumeweb/libweb";
import { CID, CID_TYPES } from "@lumeweb/libs5";
import type { moduleQuery, presentKeyData } from "@lumeweb/libkernel/module";
import { defer } from "@lumeweb/libkernel/module";
import { readableStreamToUint8Array } from "binconv";
import { getSavedRegistryEntry } from "./registry.js";
import {
networkReady,
resolveModuleRegistryEntry,
store as moduleStore,
} from "./modules.js";
import { blake3 } from "@noble/hashes/blake3";
import Worker from "./worker.js";
// WorkerLaunchFn is the type signature of the function that launches the
// worker to set up for processing a query.
type WorkerLaunchFn = () => Promise<[Worker, Err]>;
// modules is a hashmap that maps from a domain to the module that handles
// queries to that domain. It maintains the domain and URL of the module so
// that the worker doesn't need to be downloaded multiple times to keep
// launching queries.
//
// a new worker gets launched for every query.
interface Module {
domain: string;
resolvedDomain: string;
code: Uint8Array;
launchWorker: WorkerLaunchFn;
worker?: Worker;
}
// OpenQuery holds all of the information necessary for managing an open query.
interface OpenQuery {
isWorker: boolean;
isInternal: boolean;
internalHandler?: (message: OpenQueryResponse) => void;
domain: string;
source: any;
dest: Worker;
nonce: string;
origin: string;
sw?: boolean;
}
export interface OpenQueryResponse {
nonce: string;
method: string;
data: any;
err?: any;
sw?: boolean;
}
// Define the stateful variables for managing the modules. We track the set of
// queries that are in progress, the set of skapps that are known to the
// kernel, the set of modules that we've downloaded, and the set of modules
// that are actively being downloaded.
let queriesNonce = 0;
const queries = {} as { [module: string]: OpenQuery };
const modules = {} as any;
const modulesLoading = {} as any;
// Create a standard message handler for messages coming from workers.
//
// TODO: If the worker makes a mistake or has a bug that makes it seem
// unstable, we should create some sort of debug log that can be viewed from
// the kernel debug/control panel. We'll need to make sure the debug logs don't
// consume too much memory, and we'll need to terminate workers that are
// bugging out.
//
// TODO: Set up a ratelimiting system for modules making logs, we don't want
// modules to be able to pollute the kernel and cause instability by logging
// too much.
//
// TODO: Need to check that the postMessage call in respondErr isn't going to
// throw or cause issuse in the event that the worker who sent the message has
// been terminated.
//
// TODO: We probably need to have timeouts for queries, if a query doesn't send
// an update after a certain amount of time we drop it.
function handleWorkerMessage(event: MessageEvent, mod: Module, worker: Worker) {
// TODO: Use of respondErr here may not be correct, should only be using
// respondErr for functions that are expecting a response and aren't
// already part of a separate query. If they are part of a separate query
// we need to close that query out gracefully.
// Perform input verification for a worker message.
if (!("method" in event.data)) {
logErr("worker", mod.domain, "received worker message with no method");
respondErr(event, worker, true, false, "received message with no method");
return;
}
// Check whether this is a logging call.
if (event.data.method === "log") {
// Perform the input verification for logging.
if (!("data" in event.data)) {
logErr(
"worker",
mod.domain,
"received worker log message with no data field",
);
respondErr(
event,
worker,
true,
false,
"received log messsage with no data field",
);
return;
}
if (typeof event.data.data.message !== "string") {
logErr(
"worker",
mod.domain,
"worker log data.message is not of type 'string'",
);
respondErr(
event,
worker,
true,
false,
"received log messsage with no message field",
);
return;
}
if (event.data.data.isErr === undefined) {
event.data.data.isErr = false;
}
if (typeof event.data.data.isErr !== "boolean") {
logErr(
"worker",
mod.domain,
"worker log data.isErr is not of type 'boolean'",
);
respondErr(
event,
worker,
true,
false,
"received log messsage with invalid isErr field",
);
return;
}
// Send the log to the parent so that the log can be put in the
// console.
if (event.data.data.isErr === false) {
log("worker", "[" + mod.domain + "]", event.data.data.message);
} else {
logErr("worker", "[" + mod.domain + "]", event.data.data.message);
}
return;
}
// Check for a nonce - log is the only message from a worker that does not
// need a nonce.
if (!("nonce" in event.data)) {
event.data.nonce = "N/A";
logErr(
"worker",
mod.domain,
"worker sent a message with no nonce",
event.data,
);
respondErr(event, worker, true, false, "received message with no nonce");
return;
}
// Handle a version request.
if (event.data.method === "version") {
worker.postMessage({
nonce: event.data.nonce,
method: "response",
err: null,
data: {
distribution: KERNEL_DISTRO,
version: KERNEL_VERSION,
err: null,
},
});
return;
}
// Handle a call from the worker to another module.
if (event.data.method === "moduleCall") {
handleModuleCall(event, worker, mod.domain, true, false);
return;
}
// The only other methods allowed are the queryUpdate, responseUpdate,
// and response methods.
const isQueryUpdate = event.data.method === "queryUpdate";
const isResponseUpdate = event.data.method === "responseUpdate";
const isResponse = event.data.method === "response";
if (isQueryUpdate || isResponseUpdate || isResponse) {
handleModuleResponse(event, mod, worker);
return;
}
// We don't know what this message was.
logErr(
"worker",
mod.domain,
"received message from worker with unrecognized method",
);
}
// createModule will create a module from the provided worker code and domain.
// This call does not launch the worker, that should be done separately.
async function createModule(
workerCode: Uint8Array,
domain: string,
resolvedDomain: string,
): Promise<[Module | null, Err]> {
// Create the module object.
const mod: Module = {
domain,
resolvedDomain,
code: workerCode,
launchWorker: function (): Promise<[Worker, Err]> {
return launchWorker(mod);
},
};
// Start worker
const [worker, err] = await mod.launchWorker();
if (err !== null) {
return [{} as Module, addContextToErr(err, "unable to launch worker")];
}
mod.worker = worker;
return [mod, null];
}
// launchWorker will launch a worker and perform all the setup so that the
// worker is ready to receive a query.
async function launchWorker(mod: Module): Promise<[Worker, Err]> {
// Create and launch the worker.
let worker: Worker;
try {
worker = new Worker(mod.code, CID.decode(mod.resolvedDomain));
await worker.ready;
} catch (err: any) {
logErr("worker", mod.domain, "unable to create worker", mod.domain, err);
return [
{} as Worker,
addContextToErr(objAsString(err), "unable to create worker"),
];
}
// clean up memory, we don't need the code anymore
mod.code = new Uint8Array();
// Set the onmessage and onerror functions.
worker.onmessage = function (event: MessageEvent) {
handleWorkerMessage(event, mod, worker);
};
worker.onerror = function (event: ErrorEvent) {
const errStr =
objAsString(event.message) +
"\n" +
objAsString(event.error) +
"\n" +
objAsString(event);
logErr(
"worker",
mod.domain,
addContextToErr(errStr, "received onerror event"),
);
};
// Send the key to the module.
const path = "moduleKeyDerivation" + mod.domain;
const moduleKey = deriveChildKey(activeKey, path);
const msgData: presentKeyData = {
key: moduleKey,
rootPrivateKey: activeKey,
// @ts-ignore
rootKey: activeKey,
};
const msg: moduleQuery = {
method: "presentKey",
domain: "root",
data: msgData,
};
worker.postMessage(msg);
return [worker, null];
}
// handleModuleCall will handle a callModule message sent to the kernel from an
// extension or webpage.
async function handleModuleCall(
event: MessageEvent,
messagePortal: any,
callerDomain: string,
isWorker: boolean,
isInternal: false | ((message: OpenQueryResponse) => void),
) {
if (!("data" in event.data) || !("module" in event.data.data)) {
logErr(
"moduleCall",
"received moduleCall with no module field in the data",
event.data,
);
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"moduleCall is missing 'module' field: " + JSON.stringify(event.data),
);
return;
}
let validCid = false;
let isResolver = false;
if (
typeof event.data.data.module === "string" &&
CID.verify(event.data.data.module)
) {
const cid = CID.decode(event.data.data.module);
validCid = true;
if (cid.type === CID_TYPES.RESOLVER) {
isResolver = true;
}
}
if (!validCid) {
logErr("moduleCall", "received moduleCall with malformed module");
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"'module' field in moduleCall is expected to be a raw CID or a resolver CID",
);
return;
}
if (!("method" in event.data.data)) {
logErr(
"moduleCall",
"received moduleCall without a method set for the module",
);
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"no 'data.method' specified, module does not know what method to run",
);
return;
}
if (typeof event.data.data.method !== "string") {
logErr(
"moduleCall",
"received moduleCall with malformed method",
event.data,
);
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"'data.method' needs to be a string",
);
return;
}
if (event.data.data.method === "presentSeed") {
logErr(
"moduleCall",
"received malicious moduleCall - only root is allowed to use presentSeed method",
);
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"presentSeed is a privileged method, only root is allowed to use it",
);
return;
}
if (!("data" in event.data.data)) {
logErr("moduleCall", "received moduleCall with no input for the module");
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"no field data.data in moduleCall, data.data contains the module input",
);
return;
}
let moduleDomain = event.data.data.module; // Can change with overrides.
let finalModule = moduleDomain; // Can change with overrides.
if (isResolver) {
const registryFail = () => {
logErr("moduleCall", "received moduleCall with no known registry entry");
respondErr(
event,
messagePortal,
isWorker,
isInternal,
"registry entry for module is not found",
);
};
finalModule = getSavedRegistryEntry(moduleDomain);
if (!finalModule) {
if (!(await networkReady())) {
registryFail();
return;
}
let resolvedModule;
try {
resolvedModule = await resolveModuleRegistryEntry(moduleDomain);
} catch (e) {
registryFail();
return;
}
finalModule = resolvedModule;
}
}
// Define a helper function to create a new query to the module. It will
// both open a query on the module and also send an update message to the
// caller with the kernel nonce for this query so that the caller can
// perform query updates.
const newModuleQuery = function (mod: Module) {
let worker = mod.worker!;
// Get the nonce for this query. The nonce is a
// cryptographically secure string derived from a number and
// the user's seed. We use 'kernelNonceSalt' as a salt to
// namespace the nonces and make sure other processes don't
// accidentally end up using the same hashes.
const nonceSalt = new TextEncoder().encode("kernelNonceSalt");
const [nonceBytes] = encodeU64(BigInt(queriesNonce));
const noncePreimage = new Uint8Array(
nonceSalt.length + activeKey.length + nonceBytes.length,
);
noncePreimage.set(nonceSalt, 0);
noncePreimage.set(activeKey, nonceSalt.length);
noncePreimage.set(nonceBytes, nonceSalt.length + activeKey.length);
const nonce = bufToB64(sha512(noncePreimage));
queriesNonce = queriesNonce + 1;
queries[nonce] = {
isWorker,
isInternal: !!isInternal,
internalHandler: isInternal ?? undefined,
domain: callerDomain,
source: messagePortal,
dest: worker,
nonce: event.data.nonce,
origin: event.origin,
sw: event.data.sw,
} as OpenQuery;
// Send the message to the worker to start the query.
worker.postMessage({
nonce: nonce,
domain: callerDomain,
method: event.data.data.method,
data: event.data.data.data,
});
// If the caller is asking for the kernel nonce for this query,
// send the kernel nonce. We don't always send the kernel nonce
// because messages have material overhead.
if (event.data.sendKernelNonce === true) {
const msg = {
nonce: event.data.nonce,
method: "responseNonce",
data: {
nonce,
},
sw: event.data.sw,
};
if (isWorker) {
messagePortal.postMessage(msg);
} else {
messagePortal.postMessage(msg, event.origin);
}
}
};
// Check the worker pool to see if this module is already available.
if (moduleDomain in modules) {
const module = modules[moduleDomain];
newModuleQuery(module);
return;
}
// Check if another thread is already fetching the module.
if (moduleDomain in modulesLoading) {
const p = modulesLoading[moduleDomain];
p.then((errML: Err) => {
if (errML !== null) {
respondErr(
event,
messagePortal,
isWorker,
isInternal,
addContextToErr(errML, "module could not be loaded"),
);
return;
}
const module = modules[moduleDomain];
newModuleQuery(module);
});
return;
}
// Fetch the module in a background thread, and launch the query once the
// module is available.
modulesLoading[moduleDomain] = new Promise(async (resolve) => {
let cachedModule: Uint8Array | undefined;
try {
cachedModule = await (await moduleStore()).get(finalModule);
} catch {}
let moduleData: ReadableStream<Uint8Array> | Uint8Array | undefined;
if (cachedModule) {
const hash = CID.decode(finalModule).hash.hashBytes;
if (!equalBytes(hash, blake3(cachedModule))) {
logErr("corrupt module found in store: ", finalModule);
} else {
moduleData = cachedModule;
}
}
if (!moduleData) {
try {
moduleData = await downloadSmallObject(finalModule);
} catch (e) {
const err = addContextToErr(e, "unable to load module");
respondErr(event, messagePortal, isWorker, isInternal, err);
resolve(err);
delete modulesLoading[moduleDomain];
return;
}
}
// The call to download the skylink is async. That means it's possible that
// some other thread created the module successfully and already added it.
// Based on the rest of the code, this should not be possible, but we check
// for it anyway at runtime so that any concurrency bugs will be made
// visible through the `notableErrors` field.
//
// This check is mainly here as a verification that the rest of the kernel
// code is correct.
if (moduleDomain in modules) {
// Though this is an error, we do already have the module so we
// use the one we already loaded.
logErr("a module that was already loaded has been loaded");
notableErrors.push("module loading experienced a race condition");
const mod = modules[moduleDomain];
newModuleQuery(mod);
resolve(null);
return;
}
if (moduleData instanceof ReadableStream) {
try {
moduleData = await readableStreamToUint8Array(moduleData);
} catch (e) {
respondErr(event, messagePortal, isWorker, isInternal, e);
resolve(e);
delete modulesLoading[moduleDomain];
return;
}
}
if (!cachedModule) {
await (await moduleStore()).put(finalModule, moduleData);
}
if (!(moduleData instanceof Uint8Array)) {
moduleData = new TextEncoder().encode(moduleData);
}
// Create a new module.
const [mod, errCM] = await createModule(
moduleData,
moduleDomain,
finalModule,
);
if (errCM !== null) {
const err = addContextToErr(errCM, "unable to create module");
respondErr(event, messagePortal, isWorker, isInternal, err);
resolve(err);
delete modulesLoading[moduleDomain];
return;
}
modules[moduleDomain] = mod as Module;
newModuleQuery(mod as Module);
resolve(null);
delete modulesLoading[moduleDomain];
});
}
function handleModuleResponse(
event: MessageEvent,
mod: Module,
worker: Worker,
) {
// TODO: Need to figure out what to do with the errors here. Do we call
// 'respondErr'? That doesn't seem correct. It's not correct because if we
// end a query we need to let both sides know that the query was killed by
// the kernel.
// Technically the caller already computed these values, but it's easier to
// compute them again than to pass them as function args.
const isQueryUpdate = event.data.method === "queryUpdate";
const isResponse = event.data.method === "response";
// Check that the data field is present.
if (!("data" in event.data)) {
logErr(
"worker",
mod.domain,
"received response or update from worker with no data field",
);
return;
}
// Grab the query information so that we can properly relay the worker
// response to the original caller.
if (!(event.data.nonce in queries)) {
// If there's no corresponding query and this is a response, send an
// error.
if (isResponse) {
logErr("worker", mod.domain, "received response for an unknown nonce");
return;
}
// If there's no responding query and this isn't a response, it could
// just be an accident. queryUpdates and responseUpdates are async and
// can therefore be sent before both sides know that a query has been
// closed but not get processed untila afterwards.
//
// This can't happen with a 'response' message because the response
// message is the only message that can close the query, and there's
// only supposed to be one response message.
return;
}
// If the message is a query update, relay the update to the worker.
if (isQueryUpdate) {
const dest = queries[event.data.nonce].dest;
dest.postMessage({
nonce: event.data.nonce,
method: event.data.method,
data: event.data.data,
});
return;
}
// Check that the err field is being used correctly for response messages.
if (isResponse) {
// Check that the err field exists.
if (!("err" in event.data)) {
logErr(
"worker",
mod.domain,
"got response from worker with no err field",
);
return;
}
// Check that exactly one of 'err' and 'data' are null.
const errNull = event.data.err === null;
const dataNull = event.data.data === null;
if (errNull === dataNull) {
logErr("worker", mod.domain, "exactly one of err and data must be null");
return;
}
}
// We are sending either a response message or a responseUpdate message,
// all other possibilities have been handled.
const query = queries[event.data.nonce];
const sourceIsWorker = query.isWorker;
const sourceIsInternal = query.isInternal;
const internalHandler = query.internalHandler;
const sourceNonce = query.nonce;
const source = query.source;
const origin = query.origin;
const msg: OpenQueryResponse = {
nonce: sourceNonce,
method: event.data.method,
data: event.data.data,
sw: query.sw,
};
// For responses only, set an error and close out the query by deleting it
// from the query map.
if (isResponse) {
msg["err"] = event.data.err;
delete queries[event.data.nonce];
}
if (sourceIsWorker) {
source.postMessage(msg);
} else if (sourceIsInternal) {
internalHandler?.(msg);
} else {
source.postMessage(msg, origin);
}
}
function handleQueryUpdate(event: MessageEvent) {
// Check that the module still exists before sending a queryUpdate to
// the module.
if (!(event.data.nonce in queries)) {
logErr(
"auth",
"received queryUpdate but nonce is not recognized",
event.data,
queries,
);
return;
}
const dest = queries[event.data.nonce].dest;
dest.postMessage({
nonce: event.data.nonce,
method: event.data.method,
data: event.data.data,
sw: event.data.sw,
});
}
export async function internalModuleCall(
module: string,
method: string,
params = {},
): Promise<any> {
const callDefer = defer();
handleModuleCall(
{
data: {
data: {
module,
method,
data: params,
nonce: 0,
},
},
origin: "root",
} as any,
undefined,
"root",
false,
(message) => {
if (message.err) {
callDefer.reject(message.err);
return;
}
callDefer.resolve(message.data);
},
);
return callDefer.promise;
}
export {
Module,
handleModuleCall,
handleModuleResponse,
handleQueryUpdate,
modules,
modulesLoading,
queries,
};

View File

@ -1,38 +0,0 @@
const DEFAULT_MODULE_REGISTRY = new Map<string, string>(
Object.entries({
// swarm
zrjTDyEX8Mh2PdDdRj5YL2byFGrYe1ksczRwPaTRFaCGSMG:
"z2H78pADGKWPz2zWEgGKDc7jYYtSv6qBfDtKzU4Tq5zgFoejmiQD",
// peerDiscoveryRegistry
zrjD6CEchDtSex5VHjzMNSAdkJpMNfCtbxSnftgtfvtnsdY:
"z2H7AhnortTD6wL53XUdTotJZLADa7PbZCcHuSFJ6WgZ6td2bvaC",
// ircPeerDiscovery
zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ:
"z2H7D35inXTkjuxevunyq7ojv1iomXJD1svDYgkLnknk2bXc14HC",
// s5
zrjLjKVByzt233rfcjWvTQXrMfGFa11oBLydPaUk7gwnC2d:
"z2H74p9eZkzShhjfpmYLB4jpmi5qKZZHUku2eaAysWucMfcvDrpC",
// networkRegistry
zrjTCwTcK5Vco1h7cdUQKzs6yzeqm7vC5u5Lo9y1uhTyxnv:
"z2H7J3strfaEAc1kyHqMNmEPzynRipVerfCeqEhfkkcrGNNhnJUo",
}),
);
const REGISTRY_ITEM_ID = "registry";
Object.freeze(DEFAULT_MODULE_REGISTRY);
export function getSavedRegistryEntry(pubkey: string) {
const savedEntries = new Map<string, string>(
Object.entries(globalThis.localStorage.getItem(REGISTRY_ITEM_ID) ?? {}),
);
if (savedEntries.has(pubkey)) {
return savedEntries.get(pubkey) as string;
}
if (DEFAULT_MODULE_REGISTRY.has(pubkey)) {
return DEFAULT_MODULE_REGISTRY.get(pubkey) as string;
}
return null;
}

View File

@ -1,11 +0,0 @@
// Set the distribution and version of this kernel. There may be other versions
// of the kernel in the world produced by other development teams, so openly
// declaring the version number and development team allows other pieces of
// software to determine what features are or are not supported.
//
// At some point we may want something like a capabilities array, but the
// ecosystem isn't mature enough to need that.
const KERNEL_DISTRO = "Lume Web";
const KERNEL_VERSION = "0.1.0";
export { KERNEL_DISTRO, KERNEL_VERSION };

View File

@ -1,86 +0,0 @@
import { CID } from "@lumeweb/libs5";
import { defer } from "@lumeweb/libkernel/module";
type WorkerMessage = (this: Worker, ev: MessageEvent) => any;
type WorkerError = (this: Worker, ev: ErrorEvent) => any;
export default class Worker {
private _code: Uint8Array;
private _errorHandler?: WorkerError;
private _messageHandler?: WorkerMessage;
private _iframe: HTMLIFrameElement;
private _iframeDefer = defer();
private _cid: CID;
constructor(code: Uint8Array, cid: CID) {
this._code = code;
this._cid = cid;
const iframe = document.createElement("iframe");
iframe.src = this.getModuleUrl();
iframe.onload = () => {
this._postMessage({
method: "workerInit",
module: this._code,
});
};
this._iframe = iframe;
document.body.appendChild(iframe);
window.addEventListener("message", (event: MessageEvent) => {
if (event.source !== iframe.contentWindow) {
return;
}
if (event.data.method === "workerMessage") {
this._messageHandler?.(
new MessageEvent("worker", { data: event.data.data }),
);
return;
}
if (event.data.method === "workerError") {
this._errorHandler?.(
new ErrorEvent("worker", {
message: event.data.data.message,
error: event.data.data.error,
}),
);
return;
}
if (event.data.method === "workerInited") {
this._iframeDefer.resolve();
return;
}
});
}
set onmessage(handler: (this: Worker, ev: MessageEvent) => any) {
this._messageHandler = handler;
}
set onerror(handler: (this: Worker, ev: ErrorEvent) => any) {
this._errorHandler = handler;
}
get ready(): Promise<any> {
return this._iframeDefer.promise;
}
postMessage(message: any) {
this._iframeDefer.promise.then(() => {
this._postMessage(message);
});
}
_postMessage(message: any) {
this._iframe.contentWindow!.postMessage(message, this.getModuleUrl());
}
private getModuleUrl() {
return `https://${this._cid.toBase32()}.module.kernel.lumeweb.com`;
}
}