@silverbulletmd/silverbullet 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +98 -0
- package/client/asset_bundle/bundle.ts +95 -0
- package/client/data/datastore.ts +85 -0
- package/client/data/kv_primitives.ts +25 -0
- package/client/markdown_parser/constants.ts +13 -0
- package/client/plugos/event.ts +36 -0
- package/client/plugos/eventhook.ts +8 -0
- package/client/plugos/hooks/code_widget.ts +59 -0
- package/client/plugos/hooks/command.ts +104 -0
- package/client/plugos/hooks/document_editor.ts +77 -0
- package/client/plugos/hooks/event.ts +187 -0
- package/client/plugos/hooks/mq.ts +154 -0
- package/client/plugos/hooks/plug_namespace.ts +85 -0
- package/client/plugos/hooks/slash_command.ts +192 -0
- package/client/plugos/hooks/syscall.ts +66 -0
- package/client/plugos/manifest_cache.ts +67 -0
- package/client/plugos/plug.ts +99 -0
- package/client/plugos/plug_compile.ts +202 -0
- package/client/plugos/protocol.ts +40 -0
- package/client/plugos/proxy_fetch.ts +53 -0
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
- package/client/plugos/sandboxes/sandbox.ts +14 -0
- package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
- package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
- package/client/plugos/syscalls/asset.ts +35 -0
- package/client/plugos/syscalls/clientStore.ts +21 -0
- package/client/plugos/syscalls/client_code_widget.ts +12 -0
- package/client/plugos/syscalls/code_widget.ts +24 -0
- package/client/plugos/syscalls/config.ts +46 -0
- package/client/plugos/syscalls/datastore.ts +89 -0
- package/client/plugos/syscalls/editor.ts +673 -0
- package/client/plugos/syscalls/event.ts +36 -0
- package/client/plugos/syscalls/fetch.ts +128 -0
- package/client/plugos/syscalls/index.ts +102 -0
- package/client/plugos/syscalls/jsonschema.ts +69 -0
- package/client/plugos/syscalls/language.ts +23 -0
- package/client/plugos/syscalls/lua.ts +58 -0
- package/client/plugos/syscalls/markdown.ts +84 -0
- package/client/plugos/syscalls/mq.ts +52 -0
- package/client/plugos/syscalls/service_registry.ts +43 -0
- package/client/plugos/syscalls/shell.ts +39 -0
- package/client/plugos/syscalls/space.ts +139 -0
- package/client/plugos/syscalls/sync.ts +77 -0
- package/client/plugos/syscalls/system.ts +150 -0
- package/client/plugos/system.ts +201 -0
- package/client/plugos/types.ts +60 -0
- package/client/plugos/util.ts +14 -0
- package/client/plugos/worker_runtime.ts +195 -0
- package/client/space_lua/ast.ts +328 -0
- package/client/space_lua/ast_narrow.ts +81 -0
- package/client/space_lua/eval.ts +2478 -0
- package/client/space_lua/labels.ts +416 -0
- package/client/space_lua/numeric.ts +240 -0
- package/client/space_lua/parse.ts +1522 -0
- package/client/space_lua/query_collection.ts +232 -0
- package/client/space_lua/rp.ts +27 -0
- package/client/space_lua/runtime.ts +1702 -0
- package/client/space_lua/stdlib/crypto.ts +10 -0
- package/client/space_lua/stdlib/encoding.ts +19 -0
- package/client/space_lua/stdlib/format.ts +770 -0
- package/client/space_lua/stdlib/js.ts +73 -0
- package/client/space_lua/stdlib/load.ts +52 -0
- package/client/space_lua/stdlib/math.ts +193 -0
- package/client/space_lua/stdlib/net.ts +113 -0
- package/client/space_lua/stdlib/os.ts +368 -0
- package/client/space_lua/stdlib/space_lua.ts +153 -0
- package/client/space_lua/stdlib/string.ts +286 -0
- package/client/space_lua/stdlib/table.ts +401 -0
- package/client/space_lua/stdlib.ts +489 -0
- package/client/space_lua/tonumber.ts +501 -0
- package/client/space_lua/util.ts +96 -0
- package/dist/plug-compile.js +1513 -0
- package/package.json +120 -0
- package/plug-api/constants.ts +42 -0
- package/plug-api/lib/async.ts +162 -0
- package/plug-api/lib/crypto.ts +202 -0
- package/plug-api/lib/dates.ts +13 -0
- package/plug-api/lib/json.ts +136 -0
- package/plug-api/lib/limited_map.ts +72 -0
- package/plug-api/lib/memory_cache.ts +21 -0
- package/plug-api/lib/native_fetch.ts +6 -0
- package/plug-api/lib/ref.ts +275 -0
- package/plug-api/lib/resolve.ts +90 -0
- package/plug-api/lib/tags.ts +15 -0
- package/plug-api/lib/transclusion.ts +122 -0
- package/plug-api/lib/tree.ts +232 -0
- package/plug-api/lib/yaml.ts +284 -0
- package/plug-api/syscall.ts +15 -0
- package/plug-api/syscalls/asset.ts +36 -0
- package/plug-api/syscalls/client_store.ts +33 -0
- package/plug-api/syscalls/code_widget.ts +8 -0
- package/plug-api/syscalls/config.ts +58 -0
- package/plug-api/syscalls/datastore.ts +96 -0
- package/plug-api/syscalls/editor.ts +517 -0
- package/plug-api/syscalls/event.ts +47 -0
- package/plug-api/syscalls/index.ts +77 -0
- package/plug-api/syscalls/jsonschema.ts +25 -0
- package/plug-api/syscalls/language.ts +23 -0
- package/plug-api/syscalls/lua.ts +20 -0
- package/plug-api/syscalls/markdown.ts +38 -0
- package/plug-api/syscalls/mq.ts +79 -0
- package/plug-api/syscalls/shell.ts +14 -0
- package/plug-api/syscalls/space.ts +212 -0
- package/plug-api/syscalls/sync.ts +28 -0
- package/plug-api/syscalls/system.ts +102 -0
- package/plug-api/syscalls/yaml.ts +28 -0
- package/plug-api/syscalls.ts +21 -0
- package/plug-api/system_mock.ts +89 -0
- package/plug-api/types/client.ts +116 -0
- package/plug-api/types/config.ts +22 -0
- package/plug-api/types/datastore.ts +28 -0
- package/plug-api/types/event.ts +27 -0
- package/plug-api/types/index.ts +56 -0
- package/plug-api/types/manifest.ts +98 -0
- package/plug-api/types/namespace.ts +6 -0
- package/plugs/builtin_plugs.ts +14 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { parseToRef, type Ref } from "@silverbulletmd/silverbullet/lib/ref";
|
|
2
|
+
import type { Client } from "../../client.ts";
|
|
3
|
+
import type { SysCallMapping } from "../system.ts";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DocumentMeta,
|
|
7
|
+
FileMeta,
|
|
8
|
+
PageMeta,
|
|
9
|
+
} from "@silverbulletmd/silverbullet/type/index";
|
|
10
|
+
|
|
11
|
+
export function spaceReadSyscalls(client: Client): SysCallMapping {
|
|
12
|
+
return {
|
|
13
|
+
"space.listPages": (): Promise<PageMeta[]> => {
|
|
14
|
+
return client.space.fetchPageList();
|
|
15
|
+
},
|
|
16
|
+
"space.readPage": async (_ctx, name: string): Promise<string> => {
|
|
17
|
+
return (await client.space.readPage(name)).text;
|
|
18
|
+
},
|
|
19
|
+
"space.readPageWithMeta": (
|
|
20
|
+
_ctx,
|
|
21
|
+
name: string,
|
|
22
|
+
): Promise<{ text: string; meta: PageMeta }> => {
|
|
23
|
+
return client.space.readPage(name);
|
|
24
|
+
},
|
|
25
|
+
"space.readRef": async (_ctx, ref: string | Ref): Promise<string> => {
|
|
26
|
+
if (typeof ref === "string") {
|
|
27
|
+
ref = parseToRef(ref)!;
|
|
28
|
+
if (!ref) {
|
|
29
|
+
throw new Error(`Invalid ref: ${ref}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return (await client.space.readRef(ref)).text;
|
|
33
|
+
},
|
|
34
|
+
"space.pageExists": (_ctx, name: string): boolean => {
|
|
35
|
+
return client.clientSystem.allKnownFiles.has(name + ".md");
|
|
36
|
+
},
|
|
37
|
+
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
|
|
38
|
+
return client.space.getPageMeta(name);
|
|
39
|
+
},
|
|
40
|
+
"space.listPlugs": (): Promise<FileMeta[]> => {
|
|
41
|
+
return client.space.listPlugs();
|
|
42
|
+
},
|
|
43
|
+
"space.listDocuments": async (): Promise<DocumentMeta[]> => {
|
|
44
|
+
return await client.space.fetchDocumentList();
|
|
45
|
+
},
|
|
46
|
+
"space.readDocument": async (_ctx, name: string): Promise<Uint8Array> => {
|
|
47
|
+
return (await client.space.readDocument(name)).data;
|
|
48
|
+
},
|
|
49
|
+
"space.getDocumentMeta": async (
|
|
50
|
+
_ctx,
|
|
51
|
+
name: string,
|
|
52
|
+
): Promise<DocumentMeta> => {
|
|
53
|
+
return await client.space.getDocumentMeta(name);
|
|
54
|
+
},
|
|
55
|
+
// DEPRECATED, please use document versions instead, left here for backwards compatibility
|
|
56
|
+
"space.listAttachments": async (): Promise<DocumentMeta[]> => {
|
|
57
|
+
return await client.space.fetchDocumentList();
|
|
58
|
+
},
|
|
59
|
+
"space.readAttachment": async (_ctx, name: string): Promise<Uint8Array> => {
|
|
60
|
+
return (await client.space.readDocument(name)).data;
|
|
61
|
+
},
|
|
62
|
+
"space.getAttachmentMeta": async (
|
|
63
|
+
_ctx,
|
|
64
|
+
name: string,
|
|
65
|
+
): Promise<DocumentMeta> => {
|
|
66
|
+
return await client.space.getDocumentMeta(name);
|
|
67
|
+
},
|
|
68
|
+
// FS
|
|
69
|
+
"space.listFiles": (): Promise<FileMeta[]> => {
|
|
70
|
+
return client.space.spacePrimitives.fetchFileList();
|
|
71
|
+
},
|
|
72
|
+
"space.getFileMeta": (_ctx, name: string): Promise<FileMeta> => {
|
|
73
|
+
return client.space.spacePrimitives.getFileMeta(name);
|
|
74
|
+
},
|
|
75
|
+
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
|
76
|
+
return (await client.space.spacePrimitives.readFile(name)).data;
|
|
77
|
+
},
|
|
78
|
+
"space.readFileWithMeta": async (
|
|
79
|
+
_ctx,
|
|
80
|
+
name: string,
|
|
81
|
+
): Promise<{ data: Uint8Array; meta: FileMeta }> => {
|
|
82
|
+
return await client.space.spacePrimitives.readFile(name);
|
|
83
|
+
},
|
|
84
|
+
"space.fileExists": async (_ctx, name: string): Promise<boolean> => {
|
|
85
|
+
// If a full sync has successfully completed (so we know what files exist)
|
|
86
|
+
// and we have a snapshot, let's use the snapshot
|
|
87
|
+
if (
|
|
88
|
+
client.fullSyncCompleted &&
|
|
89
|
+
!client.eventedSpacePrimitives.isSnapshotEmpty()
|
|
90
|
+
) {
|
|
91
|
+
return !!client.eventedSpacePrimitives.getSnapshot()[name];
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await client.space.spacePrimitives.getFileMeta(name);
|
|
95
|
+
// If this returned the file exists
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
// Assumption: any error means the file does not exist
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function spaceWriteSyscalls(editor: Client): SysCallMapping {
|
|
106
|
+
return {
|
|
107
|
+
"space.writePage": (
|
|
108
|
+
_ctx,
|
|
109
|
+
name: string,
|
|
110
|
+
text: string,
|
|
111
|
+
): Promise<PageMeta> => {
|
|
112
|
+
return editor.space.writePage(name, text);
|
|
113
|
+
},
|
|
114
|
+
"space.deletePage": async (_ctx, name: string) => {
|
|
115
|
+
console.log("Deleting page");
|
|
116
|
+
await editor.space.deletePage(name);
|
|
117
|
+
},
|
|
118
|
+
"space.writeDocument": (
|
|
119
|
+
_ctx,
|
|
120
|
+
name: string,
|
|
121
|
+
data: Uint8Array,
|
|
122
|
+
): Promise<DocumentMeta> => {
|
|
123
|
+
return editor.space.writeDocument(name, data);
|
|
124
|
+
},
|
|
125
|
+
"space.deleteDocument": async (_ctx, name: string) => {
|
|
126
|
+
await editor.space.deleteDocument(name);
|
|
127
|
+
},
|
|
128
|
+
"space.writeFile": (
|
|
129
|
+
_ctx,
|
|
130
|
+
name: string,
|
|
131
|
+
data: Uint8Array,
|
|
132
|
+
): Promise<FileMeta> => {
|
|
133
|
+
return editor.space.spacePrimitives.writeFile(name, data);
|
|
134
|
+
},
|
|
135
|
+
"space.deleteFile": (_ctx, name: string) => {
|
|
136
|
+
return editor.space.spacePrimitives.deleteFile(name);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SysCallMapping } from "../system.ts";
|
|
2
|
+
import type { Client } from "../../client.ts";
|
|
3
|
+
|
|
4
|
+
// TODO: Reimplement this
|
|
5
|
+
export function syncSyscalls(client: Client): SysCallMapping {
|
|
6
|
+
return {
|
|
7
|
+
"sync.hasInitialSyncCompleted": (): boolean => {
|
|
8
|
+
return client.fullSyncCompleted;
|
|
9
|
+
},
|
|
10
|
+
"sync.performFileSync": (_ctx, path: string): Promise<void> => {
|
|
11
|
+
client.postServiceWorkerMessage({ type: "perform-file-sync", path });
|
|
12
|
+
return waitForServiceWorkerActivation(path);
|
|
13
|
+
},
|
|
14
|
+
"sync.performSpaceSync": (): Promise<number> => {
|
|
15
|
+
client.postServiceWorkerMessage({ type: "perform-space-sync" });
|
|
16
|
+
return waitForServiceWorkerActivation();
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function waitForServiceWorkerActivation(path?: string): Promise<any> {
|
|
22
|
+
return new Promise<any>((resolve, reject) => {
|
|
23
|
+
client.eventHook.addLocalListener(
|
|
24
|
+
"service-worker:file-sync-complete",
|
|
25
|
+
eventHandler,
|
|
26
|
+
);
|
|
27
|
+
client.eventHook.addLocalListener(
|
|
28
|
+
"service-worker:space-sync-complete",
|
|
29
|
+
eventHandler,
|
|
30
|
+
);
|
|
31
|
+
client.eventHook.addLocalListener(
|
|
32
|
+
"service-worker:sync-error",
|
|
33
|
+
errorHandler,
|
|
34
|
+
);
|
|
35
|
+
function eventHandler(data: any) {
|
|
36
|
+
// If data.path is set, we are notified about a specific file sync -> all good, even for an individual file sync
|
|
37
|
+
// If data.path is not set, we are notified about a full space sync
|
|
38
|
+
// If we were waiting for a specific path, ignore other paths
|
|
39
|
+
if (data.path && path && data.path !== path) {
|
|
40
|
+
// Event for other file sync
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// If we were waiting for a specific path, ignore other paths
|
|
44
|
+
resolve(data);
|
|
45
|
+
|
|
46
|
+
// Unsubscribe from all these events
|
|
47
|
+
client.eventHook.removeLocalListener(
|
|
48
|
+
"service-worker:file-sync-complete",
|
|
49
|
+
eventHandler,
|
|
50
|
+
);
|
|
51
|
+
client.eventHook.removeLocalListener(
|
|
52
|
+
"service-worker:space-sync-complete",
|
|
53
|
+
eventHandler,
|
|
54
|
+
);
|
|
55
|
+
client.eventHook.removeLocalListener(
|
|
56
|
+
"service-worker:sync-error",
|
|
57
|
+
errorHandler,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
function errorHandler(e: any) {
|
|
61
|
+
reject(e);
|
|
62
|
+
// Unsubscribe from all these events
|
|
63
|
+
client.eventHook.removeLocalListener(
|
|
64
|
+
"service-worker:file-sync-complete",
|
|
65
|
+
eventHandler,
|
|
66
|
+
);
|
|
67
|
+
client.eventHook.removeLocalListener(
|
|
68
|
+
"service-worker:space-sync-complete",
|
|
69
|
+
eventHandler,
|
|
70
|
+
);
|
|
71
|
+
client.eventHook.removeLocalListener(
|
|
72
|
+
"service-worker:sync-error",
|
|
73
|
+
errorHandler,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { SysCallMapping } from "../system.ts";
|
|
2
|
+
import type { Client } from "../../client.ts";
|
|
3
|
+
import { publicVersion } from "../../../public_version.ts";
|
|
4
|
+
import type { CommandDef } from "@silverbulletmd/silverbullet/type/manifest";
|
|
5
|
+
import type { SyscallMeta } from "@silverbulletmd/silverbullet/type/index";
|
|
6
|
+
|
|
7
|
+
export function systemSyscalls(
|
|
8
|
+
client: Client,
|
|
9
|
+
readOnlyMode: boolean,
|
|
10
|
+
): SysCallMapping {
|
|
11
|
+
const api: SysCallMapping = {
|
|
12
|
+
"system.invokeFunction": (
|
|
13
|
+
_ctx,
|
|
14
|
+
fullName: string, // plug.function
|
|
15
|
+
...args: any[]
|
|
16
|
+
) => {
|
|
17
|
+
const [plugName, functionName] = fullName.split(".");
|
|
18
|
+
if (!plugName || !functionName) {
|
|
19
|
+
throw Error(`Invalid function name ${fullName}`);
|
|
20
|
+
}
|
|
21
|
+
const plug = client.clientSystem.system.loadedPlugs.get(plugName);
|
|
22
|
+
if (!plug) {
|
|
23
|
+
throw Error(`Plug ${plugName} not found`);
|
|
24
|
+
}
|
|
25
|
+
const functionDef = plug.manifest!.functions[functionName];
|
|
26
|
+
if (!functionDef) {
|
|
27
|
+
throw Error(`Function ${functionName} not found`);
|
|
28
|
+
}
|
|
29
|
+
return plug.invoke(functionName, args);
|
|
30
|
+
},
|
|
31
|
+
"system.invokeFunctionOnServer": (
|
|
32
|
+
ctx,
|
|
33
|
+
fullName: string, // plug.function
|
|
34
|
+
...args: any[]
|
|
35
|
+
) => {
|
|
36
|
+
console.warn(
|
|
37
|
+
"Calling deprecated system.invokeFunctionOnServer, use system.invokeFunction instead",
|
|
38
|
+
);
|
|
39
|
+
return api["system.invokeFunction"](ctx, fullName, ...args);
|
|
40
|
+
},
|
|
41
|
+
"system.serverSyscall": (_ctx, name: string, ...args: any[]) => {
|
|
42
|
+
console.warn(
|
|
43
|
+
"Calling deprecated system.serverSyscall, use syscall instead",
|
|
44
|
+
);
|
|
45
|
+
return client.clientSystem.localSyscall(name, args);
|
|
46
|
+
},
|
|
47
|
+
"system.invokeCommand": (_ctx, name: string, args?: string[]) => {
|
|
48
|
+
console.warn("Deprecated, use editor.invokeCommand instead");
|
|
49
|
+
return client.runCommandByName(name, args);
|
|
50
|
+
},
|
|
51
|
+
"system.listCommands": (): { [key: string]: CommandDef } => {
|
|
52
|
+
const commandHook = client.clientSystem.commandHook;
|
|
53
|
+
const allCommands: { [key: string]: CommandDef } = {};
|
|
54
|
+
for (const [cmd, def] of commandHook.buildAllCommands()) {
|
|
55
|
+
allCommands[cmd] = {
|
|
56
|
+
name: def.name,
|
|
57
|
+
contexts: def.contexts,
|
|
58
|
+
priority: def.priority,
|
|
59
|
+
key: def.key,
|
|
60
|
+
mac: def.mac,
|
|
61
|
+
hide: def.hide,
|
|
62
|
+
requireMode: def.requireMode,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return allCommands;
|
|
66
|
+
},
|
|
67
|
+
"system.listSyscalls": (): SyscallMeta[] => {
|
|
68
|
+
const syscalls: SyscallMeta[] = [];
|
|
69
|
+
for (
|
|
70
|
+
const [name, info] of client.clientSystem.system.registeredSyscalls
|
|
71
|
+
) {
|
|
72
|
+
syscalls.push({
|
|
73
|
+
name,
|
|
74
|
+
requiredPermissions: info.requiredPermissions,
|
|
75
|
+
argCount: Math.max(0, info.callback.length - 1),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return syscalls;
|
|
79
|
+
},
|
|
80
|
+
"system.reloadPlugs": () => {
|
|
81
|
+
if (!client) {
|
|
82
|
+
throw new Error("Not supported");
|
|
83
|
+
}
|
|
84
|
+
return client.loadPlugs();
|
|
85
|
+
},
|
|
86
|
+
"system.reloadConfig": (): Record<string, any> => {
|
|
87
|
+
console.warn("system.reloadConfig is deprecated, it's now a no-op");
|
|
88
|
+
return client.config.values;
|
|
89
|
+
},
|
|
90
|
+
"system.loadSpaceScripts": async () => {
|
|
91
|
+
console.warn("DEPRECATED: used system.loadScripts instead");
|
|
92
|
+
await client.clientSystem.loadLuaScripts();
|
|
93
|
+
},
|
|
94
|
+
"system.loadScripts": async () => {
|
|
95
|
+
await client.clientSystem.loadLuaScripts();
|
|
96
|
+
},
|
|
97
|
+
"system.loadSpaceStyles": async () => {
|
|
98
|
+
if (!client) {
|
|
99
|
+
throw new Error("Not supported on server");
|
|
100
|
+
}
|
|
101
|
+
await client.loadCustomStyles();
|
|
102
|
+
},
|
|
103
|
+
"system.wipeClient": async (_ctx, logout = false) => {
|
|
104
|
+
await client.wipeClient();
|
|
105
|
+
if (logout) {
|
|
106
|
+
location.href = ".logout";
|
|
107
|
+
} else {
|
|
108
|
+
alert("Client wiped, feel free to navigate elsewhere");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
// DEPRECATED
|
|
112
|
+
"system.cleanDatabases": (): boolean => {
|
|
113
|
+
console.warn(
|
|
114
|
+
"system.cleanDatabses is deprecated, use Client: Wipe instead",
|
|
115
|
+
);
|
|
116
|
+
return false;
|
|
117
|
+
},
|
|
118
|
+
"system.getMode": () => {
|
|
119
|
+
return readOnlyMode ? "ro" : "rw";
|
|
120
|
+
},
|
|
121
|
+
"system.getURLPrefix": () => {
|
|
122
|
+
const url = new URL(document.baseURI);
|
|
123
|
+
|
|
124
|
+
return url.pathname;
|
|
125
|
+
},
|
|
126
|
+
"system.getBaseURI": () => {
|
|
127
|
+
return document.baseURI;
|
|
128
|
+
},
|
|
129
|
+
"system.getVersion": () => {
|
|
130
|
+
return publicVersion;
|
|
131
|
+
},
|
|
132
|
+
"system.getConfig": (_ctx, key: string, defaultValue: any = undefined) => {
|
|
133
|
+
return client.config.get(key, defaultValue);
|
|
134
|
+
},
|
|
135
|
+
// DEPRECATED
|
|
136
|
+
"system.getEnv": () => {
|
|
137
|
+
console.warn(
|
|
138
|
+
"system.getEnv is deprecated, you can assume the env to always be the client",
|
|
139
|
+
);
|
|
140
|
+
return null;
|
|
141
|
+
},
|
|
142
|
+
"system.getSpaceConfig": (_ctx, key, defaultValue?) => {
|
|
143
|
+
console.warn(
|
|
144
|
+
"system.getSpaceConfig is deprecated, use system.getConfig instead",
|
|
145
|
+
);
|
|
146
|
+
return client.config.get(key, defaultValue);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
return api;
|
|
150
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { Hook } from "./types.ts";
|
|
2
|
+
import { EventEmitter } from "./event.ts";
|
|
3
|
+
import type { SandboxFactory } from "./sandboxes/sandbox.ts";
|
|
4
|
+
import { Plug } from "./plug.ts";
|
|
5
|
+
import { InMemoryManifestCache, type ManifestCache } from "./manifest_cache.ts";
|
|
6
|
+
import {
|
|
7
|
+
builtinPlugNames,
|
|
8
|
+
builtinPlugPaths,
|
|
9
|
+
} from "../../plugs/builtin_plugs.ts";
|
|
10
|
+
|
|
11
|
+
export interface SysCallMapping {
|
|
12
|
+
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SystemEvents<HookT> = {
|
|
16
|
+
plugLoaded: (plug: Plug<HookT>) => void | Promise<void>;
|
|
17
|
+
plugUnloaded: (name: string) => void | Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Passed to every syscall, allows to pass in additional context that the syscall may use
|
|
21
|
+
export type SyscallContext = {
|
|
22
|
+
// This is the plug that is invoking the syscall,
|
|
23
|
+
// which may be undefined where this cannot be determined (e.g. when running in a NoSandbox)
|
|
24
|
+
plug?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type SyscallSignature = (
|
|
28
|
+
...args: any[]
|
|
29
|
+
) => Promise<any> | any;
|
|
30
|
+
|
|
31
|
+
type Syscall = {
|
|
32
|
+
requiredPermissions: string[];
|
|
33
|
+
callback: SyscallSignature;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SystemOptions = {
|
|
37
|
+
manifestCache?: ManifestCache<any>;
|
|
38
|
+
plugFlushTimeout?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
42
|
+
registeredSyscalls = new Map<string, Syscall>();
|
|
43
|
+
protected plugs = new Map<string, Plug<HookT>>();
|
|
44
|
+
protected enabledHooks = new Set<Hook<HookT>>();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param env either an environment or undefined for hybrid mode
|
|
48
|
+
*/
|
|
49
|
+
constructor(
|
|
50
|
+
readonly env: string | undefined = undefined,
|
|
51
|
+
readonly options: SystemOptions = {},
|
|
52
|
+
) {
|
|
53
|
+
super();
|
|
54
|
+
if (!options.manifestCache) {
|
|
55
|
+
options.manifestCache = new InMemoryManifestCache();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get loadedPlugs(): Map<string, Plug<HookT>> {
|
|
60
|
+
return this.plugs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addHook(feature: Hook<HookT>) {
|
|
64
|
+
this.enabledHooks.add(feature);
|
|
65
|
+
feature.apply(this);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerSyscalls(
|
|
69
|
+
requiredCapabilities: string[],
|
|
70
|
+
...registrationObjects: SysCallMapping[]
|
|
71
|
+
) {
|
|
72
|
+
for (const registrationObject of registrationObjects) {
|
|
73
|
+
for (const [name, callback] of Object.entries(registrationObject)) {
|
|
74
|
+
this.registeredSyscalls.set(name, {
|
|
75
|
+
requiredPermissions: requiredCapabilities,
|
|
76
|
+
callback,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Invokes a function named using the "plug.functionName" pattern, for convenience
|
|
84
|
+
* @param name name of the function (e.g. plug.doSomething)
|
|
85
|
+
* @param args an array of arguments to pass to the function
|
|
86
|
+
*/
|
|
87
|
+
invokeFunction(name: string, args: any[]): Promise<any> {
|
|
88
|
+
// Some sanity type checks
|
|
89
|
+
if (typeof name !== "string") {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`invokeFunction: function name should be a string, got ${typeof name}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(args)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`invokeFunction: args should be an array, got ${typeof args}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const [plugName, functionName] = name.split(".");
|
|
100
|
+
if (!functionName) {
|
|
101
|
+
// Sanity check
|
|
102
|
+
throw new Error(`Missing function name: ${name}`);
|
|
103
|
+
}
|
|
104
|
+
const plug = this.loadedPlugs.get(plugName);
|
|
105
|
+
if (!plug) {
|
|
106
|
+
throw new Error(`Plug ${plugName} not found invoking ${name}`);
|
|
107
|
+
}
|
|
108
|
+
return plug.invoke(functionName, args);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
localSyscall(name: string, args: any[]): Promise<any> {
|
|
112
|
+
return this.syscall({}, name, args);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
syscall(
|
|
116
|
+
ctx: SyscallContext,
|
|
117
|
+
name: string,
|
|
118
|
+
args: any[],
|
|
119
|
+
): Promise<any> {
|
|
120
|
+
const syscall = this.registeredSyscalls.get(name);
|
|
121
|
+
if (!syscall) {
|
|
122
|
+
throw Error(`Unregistered syscall ${name}`);
|
|
123
|
+
}
|
|
124
|
+
if (ctx.plug) {
|
|
125
|
+
// Only when running in a plug context do we check permissions
|
|
126
|
+
const plug = this.loadedPlugs.get(ctx.plug);
|
|
127
|
+
if (!plug) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Plug ${ctx.plug} not found while attempting to invoke ${name}}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (const permission of syscall.requiredPermissions) {
|
|
133
|
+
if (!plug.grantedPermissions.includes(permission)) {
|
|
134
|
+
throw Error(`Missing permission '${permission}' for syscall ${name}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve(syscall.callback(ctx, ...args));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param cacheKey Used to cache the manifest of the worker. Should be equal to the filepath
|
|
143
|
+
* @param cacheHash Used to determine if the manifest is up to date. Should be `lastModified`
|
|
144
|
+
*/
|
|
145
|
+
async loadPlug(
|
|
146
|
+
sandboxFactory: SandboxFactory<HookT>,
|
|
147
|
+
cacheKey: string,
|
|
148
|
+
cacheHash: number = -1,
|
|
149
|
+
): Promise<Plug<HookT>> {
|
|
150
|
+
const plug = await Plug.createLazily(
|
|
151
|
+
this,
|
|
152
|
+
cacheKey,
|
|
153
|
+
cacheHash,
|
|
154
|
+
sandboxFactory,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const manifest = plug.manifest;
|
|
158
|
+
|
|
159
|
+
// This depends on cacheKey being the file path
|
|
160
|
+
if (
|
|
161
|
+
!builtinPlugPaths.includes(cacheKey) &&
|
|
162
|
+
builtinPlugNames.includes(manifest.name)
|
|
163
|
+
) {
|
|
164
|
+
plug.stop();
|
|
165
|
+
throw new Error("Plug tried to overwrite internal plug");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validate the manifest
|
|
169
|
+
let errors: string[] = [];
|
|
170
|
+
for (const feature of this.enabledHooks) {
|
|
171
|
+
errors = [...errors, ...feature.validateManifest(manifest)];
|
|
172
|
+
}
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
throw new Error(`Invalid manifest: ${errors.join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
if (this.plugs.has(manifest.name)) {
|
|
177
|
+
this.unload(manifest.name);
|
|
178
|
+
}
|
|
179
|
+
console.log("Activated plug", manifest.name);
|
|
180
|
+
this.plugs.set(manifest.name, plug);
|
|
181
|
+
|
|
182
|
+
await this.emit("plugLoaded", plug);
|
|
183
|
+
return plug;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
unload(name: string) {
|
|
187
|
+
const plug = this.plugs.get(name);
|
|
188
|
+
if (!plug) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
plug.stop();
|
|
192
|
+
this.emit("plugUnloaded", name);
|
|
193
|
+
this.plugs.delete(name);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
unloadAll(): Promise<void[]> {
|
|
197
|
+
return Promise.all(
|
|
198
|
+
Array.from(this.plugs.keys()).map(this.unload.bind(this)),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { System } from "./system.ts";
|
|
2
|
+
import type { AssetJson } from "../asset_bundle/bundle.ts";
|
|
3
|
+
|
|
4
|
+
/** The generic top level of a plug manifest file.
|
|
5
|
+
* Defines plug metadata and functions.
|
|
6
|
+
*/
|
|
7
|
+
export interface Manifest<HookT> {
|
|
8
|
+
/** The plug's name. Typically this is the name of the manifest file, without the file extension. */
|
|
9
|
+
name: string;
|
|
10
|
+
|
|
11
|
+
/** A list of syscall permissions required for this plug to function.
|
|
12
|
+
*
|
|
13
|
+
* Possible values:
|
|
14
|
+
* - `fetch`: enables `fetch` function. (see: plug-api/plugos-syscall/fetch.ts, and plug-api/lib/fetch.ts)
|
|
15
|
+
* - `shell`: enables the `shell.run` syscall. (see: plug-api/plugos-syscall/shell.ts)
|
|
16
|
+
*/
|
|
17
|
+
requiredPermissions?: string[];
|
|
18
|
+
|
|
19
|
+
/** A list of files or glob patterns that should be bundled with the plug.
|
|
20
|
+
*
|
|
21
|
+
* These files will be accessible through the `asset.readAsset` function.
|
|
22
|
+
*
|
|
23
|
+
* see: plug-api/plugos-syscall/asset.ts#readAsset
|
|
24
|
+
*/
|
|
25
|
+
assets?: string[] | AssetJson;
|
|
26
|
+
|
|
27
|
+
/** A map of function names to definitions. Declared functions are public, and may be associated with various hooks
|
|
28
|
+
*
|
|
29
|
+
* see: common/manifest.ts#SilverBulletHooks
|
|
30
|
+
*/
|
|
31
|
+
functions: Record<string, FunctionDef<HookT>>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A map of configuration options for the plug (to be merged with the system configuration).
|
|
35
|
+
*/
|
|
36
|
+
config?: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */
|
|
40
|
+
export type FunctionDef<HookT> = {
|
|
41
|
+
/** A function path, in the form `${relativeFilename}:${functionName}`.
|
|
42
|
+
*
|
|
43
|
+
* During compilation (see `../build_plugs.ts`) the function is read from the file and inlined into the plug bundle.
|
|
44
|
+
*
|
|
45
|
+
* This field and `FunctionDef.redirect` are mutually exclusive.
|
|
46
|
+
*/
|
|
47
|
+
path?: string;
|
|
48
|
+
|
|
49
|
+
/** A function from another plug, in the form `${plugName}.${functionName}` that will be attached to the given hooks. */
|
|
50
|
+
redirect?: string;
|
|
51
|
+
|
|
52
|
+
/** Environments where this plug is allowed to run, current may be one of "cli", "server", or "client". */
|
|
53
|
+
env?: string;
|
|
54
|
+
} & HookT;
|
|
55
|
+
|
|
56
|
+
export interface Hook<HookT> {
|
|
57
|
+
validateManifest(manifest: Manifest<HookT>): string[];
|
|
58
|
+
|
|
59
|
+
apply(system: System<HookT>): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if an object is sendable across the plugos worker boundary.
|
|
3
|
+
*
|
|
4
|
+
* @param o - The object to check.
|
|
5
|
+
* @returns `true` if the object is sendable, `false` otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function isSendable(o: any): boolean {
|
|
8
|
+
try {
|
|
9
|
+
structuredClone(o);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|