diff --git a/build_web.ts b/build_web.ts index ef07bfaf..59562ad2 100644 --- a/build_web.ts +++ b/build_web.ts @@ -42,9 +42,6 @@ export async function copyAssets(dist: string) { await copy("web/auth.html", `${dist}/auth.html`, { overwrite: true, }); - await copy("web/logout.html", `${dist}/logout.html`, { - overwrite: true, - }); await copy("web/images/favicon.png", `${dist}/favicon.png`, { overwrite: true, }); @@ -84,7 +81,7 @@ async function buildCopyBundleAssets() { console.log("Now ESBuilding the client and service workers..."); - await esbuild.build({ + const result = await esbuild.build({ entryPoints: [ { in: "web/boot.ts", @@ -102,6 +99,7 @@ async function buildCopyBundleAssets() { sourcemap: "linked", minify: true, jsxFactory: "h", + // metafile: true, jsx: "automatic", jsxFragment: "Fragment", jsxImportSource: "https://esm.sh/preact@10.11.1", @@ -111,6 +109,11 @@ async function buildCopyBundleAssets() { }), }); + if (result.metafile) { + const text = await esbuild.analyzeMetafile(result.metafile!); + console.log("Bundle info", text); + } + // Patch the service_worker {{CACHE_NAME}} let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js"); swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`); diff --git a/cli/plug_run.test.ts b/cli/plug_run.test.ts index 826cd86c..e777c6fc 100644 --- a/cli/plug_run.test.ts +++ b/cli/plug_run.test.ts @@ -5,14 +5,12 @@ import { runPlug } from "./plug_run.ts"; import assets from "../dist/plug_asset_bundle.json" assert { type: "json" }; import { assertEquals } from "../test_deps.ts"; import { path } from "../common/deps.ts"; +import { MemoryKvPrimitives } from "../plugos/lib/memory_kv_primitives.ts"; Deno.test("Test plug run", { sanitizeResources: false, sanitizeOps: false, }, async () => { - // const tempDir = await Deno.makeTempDir(); - const tempDbFile = await Deno.makeTempFile({ suffix: ".db" }); - const assetBundle = new AssetBundle(assets); const testFolder = path.dirname(new URL(import.meta.url).pathname); @@ -31,11 +29,11 @@ Deno.test("Test plug run", { "test.run", [], assetBundle, + new MemoryKvPrimitives(), ), "Hello", ); // await Deno.remove(tempDir, { recursive: true }); esbuild.stop(); - await Deno.remove(tempDbFile); }); diff --git a/cli/plug_run.ts b/cli/plug_run.ts index 4c40249c..03fd3afb 100644 --- a/cli/plug_run.ts +++ b/cli/plug_run.ts @@ -4,29 +4,23 @@ import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { sleep } from "$sb/lib/async.ts"; import { ServerSystem } from "../server/server_system.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; -import { determineDatabaseBackend } from "../server/db_backend.ts"; import { EndpointHook } from "../plugos/hooks/endpoint.ts"; -import { determineShellBackend } from "../server/shell_backend.ts"; +import { LocalShell } from "../server/shell_backend.ts"; import { Hono } from "../server/deps.ts"; +import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; export async function runPlug( spacePath: string, functionName: string | undefined, args: string[] = [], builtinAssetBundle: AssetBundle, - httpServerPort = 3123, - httpHostname = "127.0.0.1", + kvPrimitives: KvPrimitives, + httpServerPort?: number, + httpHostname?: string, ) { const serverController = new AbortController(); const app = new Hono(); - const dbBackend = await determineDatabaseBackend(spacePath); - - if (!dbBackend) { - console.error("Cannot run plugs in databaseless mode."); - return; - } - const endpointHook = new EndpointHook("/_/"); const serverSystem = new ServerSystem( @@ -34,23 +28,25 @@ export async function runPlug( new DiskSpacePrimitives(spacePath), builtinAssetBundle, ), - dbBackend, - determineShellBackend(spacePath), + kvPrimitives, + new LocalShell(spacePath), + false, ); await serverSystem.init(true); app.use((context, next) => { return endpointHook.handleRequest(serverSystem.system!, context, next); }); - Deno.serve({ - hostname: httpHostname, - port: httpServerPort, - signal: serverController.signal, - }, app.fetch); + if (httpHostname && httpServerPort) { + Deno.serve({ + hostname: httpHostname, + port: httpServerPort, + signal: serverController.signal, + }, app.fetch); + } if (functionName) { const result = await serverSystem.system.invokeFunction(functionName, args); await serverSystem.close(); - serverSystem.kvPrimitives.close(); serverController.abort(); return result; } else { diff --git a/cmd/plug_run.ts b/cmd/plug_run.ts index 2bb191df..44262dde 100644 --- a/cmd/plug_run.ts +++ b/cmd/plug_run.ts @@ -4,6 +4,7 @@ import assets from "../dist/plug_asset_bundle.json" assert { type: "json", }; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; +import { determineDatabaseBackend } from "../server/db_backend.ts"; export async function plugRunCommand( { @@ -20,18 +21,28 @@ export async function plugRunCommand( spacePath = path.resolve(spacePath); console.log("Space path", spacePath); console.log("Function to run:", functionName, "with arguments", args); + + const kvPrimitives = await determineDatabaseBackend(spacePath); + + if (!kvPrimitives) { + console.error("Cannot run plugs in databaseless mode."); + return; + } + try { const result = await runPlug( spacePath, functionName, args, new AssetBundle(assets), + kvPrimitives, port, hostname, ); if (result) { console.log("Output", result); } + kvPrimitives.close(); Deno.exit(0); } catch (e: any) { console.error(e.message); diff --git a/cmd/server.ts b/cmd/server.ts index d5fb0aa2..5c2d8268 100644 --- a/cmd/server.ts +++ b/cmd/server.ts @@ -10,6 +10,8 @@ import { sleep } from "$sb/lib/async.ts"; import { determineDatabaseBackend } from "../server/db_backend.ts"; import { SpaceServerConfig } from "../server/instance.ts"; +import { runPlug } from "../cli/plug_run.ts"; +import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts"; export async function serveCommand( options: { @@ -41,6 +43,8 @@ export async function serveCommand( const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY"); + const readOnly = !!Deno.env.get("SB_READ_ONLY"); + if (syncOnly) { console.log("Running in sync-only mode (no backend processing)"); } @@ -75,6 +79,9 @@ export async function serveCommand( const [user, pass] = userAuth.split(":"); userCredentials = { user, pass }; } + + const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local"; + const configs = new Map(); configs.set("*", { hostname, @@ -82,15 +89,29 @@ export async function serveCommand( auth: userCredentials, authToken: Deno.env.get("SB_AUTH_TOKEN"), syncOnly, + readOnly, + shellBackend: backendConfig, clientEncryption, pagesPath: folder, }); + const plugAssets = new AssetBundle(plugAssetBundle as AssetJson); + + if (readOnly) { + console.log("Indexing the space first. Hang on..."); + await runPlug( + folder, + "index.reindexSpace", + [], + plugAssets, + new PrefixedKvPrimitives(baseKvPrimitives, ["*"]), + ); + } const httpServer = new HttpServer({ hostname, port, clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson), - plugAssetBundle: new AssetBundle(plugAssetBundle as AssetJson), + plugAssetBundle: plugAssets, baseKvPrimitives, keyFile: options.key, certFile: options.cert, diff --git a/common/spaces/ro_space_primitives.ts b/common/spaces/ro_space_primitives.ts new file mode 100644 index 00000000..e6dafce5 --- /dev/null +++ b/common/spaces/ro_space_primitives.ts @@ -0,0 +1,38 @@ +import { FileMeta } from "$sb/types.ts"; +import { SpacePrimitives } from "./space_primitives.ts"; + +export class ReadOnlySpacePrimitives implements SpacePrimitives { + wrapped: SpacePrimitives; + constructor(wrapped: SpacePrimitives) { + this.wrapped = wrapped; + } + async fetchFileList(): Promise { + return (await this.wrapped.fetchFileList()).map((f: FileMeta) => ({ + ...f, + perm: "ro", + })); + } + async readFile(name: string): Promise<{ meta: FileMeta; data: Uint8Array }> { + const { meta, data } = await this.wrapped.readFile(name); + return { + meta: { + ...meta, + perm: "ro", + }, + data, + }; + } + async getFileMeta(name: string): Promise { + const meta = await this.wrapped.getFileMeta(name); + return { + ...meta, + perm: "ro", + }; + } + writeFile(): Promise { + throw new Error("Read only space, not allowed to write"); + } + deleteFile(): Promise { + throw new Error("Read only space, not allowed to delete"); + } +} diff --git a/web/syscalls/system.ts b/common/syscalls/system.ts similarity index 87% rename from web/syscalls/system.ts rename to common/syscalls/system.ts index 8e4be3bb..84ed8a3f 100644 --- a/web/syscalls/system.ts +++ b/common/syscalls/system.ts @@ -1,10 +1,11 @@ import { SysCallMapping, System } from "../../plugos/system.ts"; -import type { Client } from "../client.ts"; -import { CommandDef } from "../hooks/command.ts"; -import { proxySyscall } from "./util.ts"; +import type { Client } from "../../web/client.ts"; +import { CommandDef } from "../../web/hooks/command.ts"; +import { proxySyscall } from "../../web/syscalls/util.ts"; export function systemSyscalls( system: System, + readOnlyMode: boolean, client?: Client, ): SysCallMapping { const api: SysCallMapping = { @@ -64,6 +65,9 @@ export function systemSyscalls( "system.getEnv": () => { return system.env; }, + "system.getMode": () => { + return readOnlyMode ? "ro" : "rw"; + }, }; return api; } diff --git a/plug-api/silverbullet-syscall/debug.ts b/plug-api/silverbullet-syscall/debug.ts index 7c833b10..2c1db5b6 100644 --- a/plug-api/silverbullet-syscall/debug.ts +++ b/plug-api/silverbullet-syscall/debug.ts @@ -3,3 +3,10 @@ import { syscall } from "./syscall.ts"; export function resetClient() { return syscall("debug.resetClient"); } + +/** + * Wipes the entire state KV store and the entire space KV store. + */ +export function cleanup() { + return syscall("debug.cleanup"); +} diff --git a/plug-api/silverbullet-syscall/system.ts b/plug-api/silverbullet-syscall/system.ts index e78e3201..5bc47c60 100644 --- a/plug-api/silverbullet-syscall/system.ts +++ b/plug-api/silverbullet-syscall/system.ts @@ -26,3 +26,7 @@ export function reloadPlugs() { export function getEnv(): Promise { return syscall("system.getEnv"); } + +export function getMode(): Promise<"ro" | "rw"> { + return syscall("system.getMode"); +} diff --git a/plugos/compile.ts b/plugos/compile.ts index 0155cd7c..41ff3881 100644 --- a/plugos/compile.ts +++ b/plugos/compile.ts @@ -101,11 +101,6 @@ setupMessageListener(functionMapping, manifest); metafile: options.info, treeShaking: true, plugins: [ - // { - // name: "json", - // setup: (build) => - // build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })), - // }, ...denoPlugins({ // TODO do this differently importMapURL: options.importMap || diff --git a/plugos/syscalls/datastore.ts b/plugos/syscalls/datastore.ts index 95ca71bd..84ec7cd0 100644 --- a/plugos/syscalls/datastore.ts +++ b/plugos/syscalls/datastore.ts @@ -7,7 +7,26 @@ import type { SysCallMapping } from "../system.ts"; * @param ds the datastore to wrap * @param prefix prefix to scope all keys to to which the plug name will be appended */ -export function dataStoreSyscalls(ds: DataStore): SysCallMapping { +export function dataStoreReadSyscalls(ds: DataStore): SysCallMapping { + return { + "datastore.batchGet": ( + _ctx, + keys: KvKey[], + ): Promise<(any | undefined)[]> => { + return ds.batchGet(keys); + }, + + "datastore.get": (_ctx, key: KvKey): Promise => { + return ds.get(key); + }, + + "datastore.query": async (_ctx, query: KvQuery): Promise => { + return (await ds.query(query)); + }, + }; +} + +export function dataStoreWriteSyscalls(ds: DataStore): SysCallMapping { return { "datastore.delete": (_ctx, key: KvKey) => { return ds.delete(key); @@ -25,21 +44,6 @@ export function dataStoreSyscalls(ds: DataStore): SysCallMapping { return ds.batchDelete(keys); }, - "datastore.batchGet": ( - _ctx, - keys: KvKey[], - ): Promise<(any | undefined)[]> => { - return ds.batchGet(keys); - }, - - "datastore.get": (_ctx, key: KvKey): Promise => { - return ds.get(key); - }, - - "datastore.query": async (_ctx, query: KvQuery): Promise => { - return (await ds.query(query)); - }, - "datastore.queryDelete": (_ctx, query: KvQuery): Promise => { return ds.queryDelete(query); }, diff --git a/plugs/editor/account.ts b/plugs/editor/account.ts deleted file mode 100644 index 4616c01b..00000000 --- a/plugs/editor/account.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { editor } from "$sb/syscalls.ts"; - -export async function accountLogoutCommand() { - await editor.openUrl("/.client/logout.html", true); -} diff --git a/plugs/editor/broken_links.ts b/plugs/editor/broken_links.ts deleted file mode 100644 index 671be0c9..00000000 --- a/plugs/editor/broken_links.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { traverseTree } from "../../plug-api/lib/tree.ts"; -import { editor, markdown, space } from "$sb/syscalls.ts"; -import { parsePageRef } from "$sb/lib/page.ts"; - -export async function brokenLinksCommand() { - const pageName = "BROKEN LINKS"; - await editor.flashNotification("Scanning your space..."); - const allPages = await space.listPages(); - const allPagesMap = new Map(allPages.map((p) => [p.name, true])); - const brokenLinks: { page: string; link: string; pos: number }[] = []; - for (const pageMeta of allPages) { - const text = await space.readPage(pageMeta.name); - const tree = await markdown.parseMarkdown(text); - traverseTree(tree, (tree) => { - if (tree.type === "WikiLinkPage") { - // Add the prefix in the link text - const { page: pageName } = parsePageRef(tree.children![0].text!); - if (pageName.startsWith("!")) { - return true; - } - if ( - pageName && !pageName.startsWith("{{") - ) { - if (!allPagesMap.has(pageName)) { - brokenLinks.push({ - page: pageMeta.name, - link: pageName, - pos: tree.from!, - }); - } - } - } - if (tree.type === "PageRef") { - const pageName = tree.children![0].text!.slice(2, -2); - if (pageName.startsWith("!")) { - return true; - } - if (!allPagesMap.has(pageName)) { - brokenLinks.push({ - page: pageMeta.name, - link: pageName, - pos: tree.from!, - }); - } - } - - return false; - }); - } - const lines: string[] = []; - for (const brokenLink of brokenLinks) { - lines.push( - `* [[${brokenLink.page}@${brokenLink.pos}]]: ${brokenLink.link}`, - ); - } - await space.writePage(pageName, lines.join("\n")); - await editor.navigate({ page: pageName }); -} diff --git a/plugs/editor/clean.ts b/plugs/editor/clean.ts new file mode 100644 index 00000000..1b463e96 --- /dev/null +++ b/plugs/editor/clean.ts @@ -0,0 +1,14 @@ +import { debug, editor } from "$sb/syscalls.ts"; + +export async function cleanCommand() { + if ( + !await editor.confirm( + "This will remove all your locally cached data and authentication cookies. Are you sure?", + ) + ) { + return; + } + await editor.flashNotification("Now wiping all state and logging out..."); + await debug.cleanup(); + await editor.openUrl("/.auth?logout", true); +} diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index 988f3602..a88d0895 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -35,10 +35,12 @@ functions: path: "./page.ts:deletePage" command: name: "Page: Delete" + requireMode: rw copyPage: path: "./page.ts:copyPage" command: name: "Page: Copy" + requireMode: rw # Completion pageComplete: @@ -54,6 +56,7 @@ functions: path: editor.ts:reloadSettingsAndCommands command: name: "System: Reload Settings and Commands" + requireMode: rw # Navigation linkNavigate: @@ -89,22 +92,26 @@ functions: name: "Text: Quote Selection" key: "Ctrl-Shift-." mac: "Cmd-Shift-." + requireMode: rw listifySelection: path: ./text.ts:listifySelection command: name: "Text: Listify Selection" key: "Ctrl-Shift-8" mac: "Cmd-Shift-8" + requireMode: rw numberListifySelection: path: ./text.ts:numberListifySelection command: name: "Text: Number Listify Selection" + requireMode: rw linkSelection: path: ./text.ts:linkSelection command: name: "Text: Link Selection" key: "Ctrl-Shift-k" mac: "Cmd-Shift-k" + requireMode: rw bold: path: ./text.ts:wrapSelection command: @@ -112,6 +119,7 @@ functions: key: "Ctrl-b" mac: "Cmd-b" wrapper: "**" + requireMode: rw italic: path: ./text.ts:wrapSelection command: @@ -119,23 +127,27 @@ functions: key: "Ctrl-i" mac: "Cmd-i" wrapper: "_" + requireMode: rw strikethrough: path: ./text.ts:wrapSelection command: name: "Text: Strikethrough" key: "Ctrl-Shift-s" wrapper: "~~" + requireMode: rw marker: path: ./text.ts:wrapSelection command: name: "Text: Marker" key: "Alt-m" wrapper: "==" + requireMode: rw centerCursor: path: "./editor.ts:centerCursorCommand" command: name: "Navigate: Center Cursor" key: "Ctrl-Alt-l" + requireMode: rw # Debug commands parseCommand: @@ -150,6 +162,7 @@ functions: name: "Link: Unfurl" key: "Ctrl-Shift-u" mac: "Cmd-Shift-u" + requireMode: rw contexts: - NakedURL @@ -180,11 +193,6 @@ functions: events: - editor:modeswitch - brokenLinksCommand: - path: ./broken_links.ts:brokenLinksCommand - command: - name: "Broken Links: Show" - # Random stuff statsCommand: path: ./stats.ts:statsCommand @@ -210,14 +218,15 @@ functions: name: "Help: Getting Started" accountLogoutCommand: - path: ./account.ts:accountLogoutCommand + path: clean.ts:cleanCommand command: - name: "Account: Logout" + name: "Clear Local Storage & Logout" uploadFileCommand: path: ./upload.ts:uploadFile command: name: "Upload: File" + requireMode: rw # Outline commands outlineMoveUp: @@ -225,24 +234,28 @@ functions: command: name: "Outline: Move Up" key: "Alt-ArrowUp" + requireMode: rw outlineMoveDown: path: ./outline.ts:moveItemDown command: name: "Outline: Move Down" key: "Alt-ArrowDown" + requireMode: rw outlineIndent: path: ./outline.ts:indentItem command: name: "Outline: Move Right" key: "Alt->" + requireMode: rw outlineOutdent: path: ./outline.ts:outdentItem command: name: "Outline: Move Left" key: "Alt-<" + requireMode: rw # Outline folding commands foldCommand: diff --git a/plugs/federation/federation.plug.yaml b/plugs/federation/federation.plug.yaml index 9e30fd7a..72ed6c5e 100644 --- a/plugs/federation/federation.plug.yaml +++ b/plugs/federation/federation.plug.yaml @@ -33,4 +33,5 @@ functions: importLibraryCommand: path: library.ts:importLibraryCommand command: - name: "Library: Import" \ No newline at end of file + name: "Library: Import" + requireMode: rw diff --git a/plugs/index/api.ts b/plugs/index/api.ts index 3d941a72..377d9132 100644 --- a/plugs/index/api.ts +++ b/plugs/index/api.ts @@ -2,7 +2,7 @@ import { datastore } from "$sb/syscalls.ts"; import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts"; import { QueryProviderEvent } from "$sb/app_event.ts"; import { builtins } from "./builtins.ts"; -import { AttributeObject, determineType } from "./attributes.ts"; +import { determineType } from "./attributes.ts"; import { ttlCache } from "$sb/lib/memory_cache.ts"; const indexKey = "idx"; @@ -47,12 +47,17 @@ export async function clearPageIndex(page: string): Promise { } /** - * Clears the entire datastore for this indexKey plug + * Clears the entire page index */ export async function clearIndex(): Promise { const allKeys: KvKey[] = []; for ( - const { key } of await datastore.query({ prefix: [] }) + const { key } of await datastore.query({ prefix: [indexKey] }) + ) { + allKeys.push(key); + } + for ( + const { key } of await datastore.query({ prefix: [pageKey] }) ) { allKeys.push(key); } diff --git a/plugs/index/builtins.ts b/plugs/index/builtins.ts index 7248fd5c..82050421 100644 --- a/plugs/index/builtins.ts +++ b/plugs/index/builtins.ts @@ -1,7 +1,6 @@ import { ObjectValue } from "$sb/types.ts"; +import { system } from "$sb/syscalls.ts"; import { indexObjects } from "./api.ts"; -import { AttributeObject } from "./attributes.ts"; -import { TagObject } from "./tags.ts"; export const builtinPseudoPage = ":builtin:"; @@ -92,6 +91,10 @@ export const builtins: Record> = { }; export async function loadBuiltinsIntoIndex() { + if (await system.getMode() === "ro") { + console.log("Running in read-only mode, not loading builtins"); + return; + } console.log("Loading builtins attributes into index"); const allObjects: ObjectValue[] = []; for (const [tagName, attributes] of Object.entries(builtins)) { diff --git a/plugs/index/command.ts b/plugs/index/command.ts index 8fba4bd8..30a8e829 100644 --- a/plugs/index/command.ts +++ b/plugs/index/command.ts @@ -11,6 +11,10 @@ export async function reindexCommand() { } export async function reindexSpace() { + if (await system.getMode() === "ro") { + console.info("Not reindexing because we're in read-only mode"); + return; + } console.log("Clearing page index..."); // Executed this way to not have to embed the search plug code here await system.invokeFunction("index.clearIndex"); @@ -55,6 +59,10 @@ export async function processIndexQueue(messages: MQMessage[]) { } export async function parseIndexTextRepublish({ name, text }: IndexEvent) { + if (await system.getMode() === "ro") { + console.info("Not reindexing", name, "because we're in read-only mode"); + return; + } const parsed = await markdown.parseMarkdown(text); if (isTemplate(text)) { diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index 50106730..1165b36d 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -51,6 +51,7 @@ functions: path: "./command.ts:reindexCommand" command: name: "Space: Reindex" + requireMode: rw processIndexQueue: path: ./command.ts:processIndexQueue mqSubscriptions: @@ -142,16 +143,19 @@ functions: mac: Cmd-Alt-r key: Ctrl-Alt-r page: "" + requireMode: rw renamePrefixCommand: path: "./refactor.ts:renamePrefixCommand" command: name: "Page: Batch Rename Prefix" + requireMode: rw # Refactoring Commands extractToPageCommand: path: ./refactor.ts:extractToPageCommand command: name: "Page: Extract" + requireMode: rw # TOC tocWidget: diff --git a/plugs/plug-manager/plug-manager.plug.yaml b/plugs/plug-manager/plug-manager.plug.yaml index 03dfb89d..8954beaf 100644 --- a/plugs/plug-manager/plug-manager.plug.yaml +++ b/plugs/plug-manager/plug-manager.plug.yaml @@ -8,6 +8,7 @@ functions: name: "Plugs: Update" key: "Ctrl-Shift-p" mac: "Cmd-Shift-p" + requireMode: rw getPlugHTTPS: path: "./plugmanager.ts:getPlugHTTPS" events: @@ -24,3 +25,4 @@ functions: path: ./plugmanager.ts:addPlugCommand command: name: "Plugs: Add" + requireMode: rw diff --git a/plugs/share/share.plug.yaml b/plugs/share/share.plug.yaml index ce7b2b0f..621a8d27 100644 --- a/plugs/share/share.plug.yaml +++ b/plugs/share/share.plug.yaml @@ -5,4 +5,5 @@ functions: command: name: "Share: Publish" key: "Ctrl-s" - mac: "Cmd-s" \ No newline at end of file + mac: "Cmd-s" + requireMode: rw diff --git a/plugs/tasks/tasks.plug.yaml b/plugs/tasks/tasks.plug.yaml index d8ca28fa..049289af 100644 --- a/plugs/tasks/tasks.plug.yaml +++ b/plugs/tasks/tasks.plug.yaml @@ -17,11 +17,13 @@ functions: command: name: "Task: Cycle State" key: Alt-t + requireMode: rw taskPostponeCommand: path: ./task.ts:postponeCommand command: name: "Task: Postpone" key: Alt-+ + requireMode: rw contexts: - DeadlineDate previewTaskToggle: @@ -37,4 +39,5 @@ functions: removeCompletedTasksCommand: path: task.ts:removeCompletedTasksCommand command: - name: "Task: Remove Completed" \ No newline at end of file + name: "Task: Remove Completed" + requireMode: rw diff --git a/plugs/template/template.plug.yaml b/plugs/template/template.plug.yaml index cf66eae0..4502b650 100644 --- a/plugs/template/template.plug.yaml +++ b/plugs/template/template.plug.yaml @@ -44,6 +44,7 @@ functions: command: name: "Page: From Template" key: "Alt-Shift-t" + requireMode: rw # Lint diff --git a/server/http_server.ts b/server/http_server.ts index 1d90f881..8cf57932 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -9,7 +9,6 @@ import { import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { FileMeta } from "$sb/types.ts"; import { ShellRequest } from "./rpc.ts"; -import { determineShellBackend } from "./shell_backend.ts"; import { SpaceServer, SpaceServerConfig } from "./instance.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts"; @@ -58,7 +57,6 @@ export class HttpServer { async bootSpaceServer(config: SpaceServerConfig): Promise { const spaceServer = new SpaceServer( config, - determineShellBackend(config.pagesPath), this.plugAssetBundle, new PrefixedKvPrimitives(this.baseKvPrimitives, [ config.namespace, @@ -119,6 +117,9 @@ export class HttpServer { ).replaceAll( "{{SYNC_ONLY}}", spaceServer.syncOnly ? "true" : "false", + ).replaceAll( + "{{READ_ONLY}}", + spaceServer.readOnly ? "true" : "false", ).replaceAll( "{{CLIENT_ENCRYPTION}}", spaceServer.clientEncryption ? "true" : "false", @@ -217,6 +218,7 @@ export class HttpServer { JSON.stringify([ spaceServer.clientEncryption, spaceServer.syncOnly, + spaceServer.readOnly, ]), ), ); @@ -510,7 +512,10 @@ export class HttpServer { const req = c.req; const name = req.param("path")!; const spaceServer = await this.ensureSpaceServer(req); - console.log("Saving file", name); + if (spaceServer.readOnly) { + return c.text("Read only mode, no writes allowed", 405); + } + console.log("Writing file", name); if (name.startsWith(".")) { // Don't expose hidden files return c.text("Forbidden", 403); @@ -533,6 +538,9 @@ export class HttpServer { const req = c.req; const name = req.param("path")!; const spaceServer = await this.ensureSpaceServer(req); + if (spaceServer.readOnly) { + return c.text("Read only mode, no writes allowed", 405); + } console.log("Deleting file", name); if (name.startsWith(".")) { // Don't expose hidden files diff --git a/server/instance.ts b/server/instance.ts index 7134b202..8bcd633e 100644 --- a/server/instance.ts +++ b/server/instance.ts @@ -1,6 +1,7 @@ import { SilverBulletHooks } from "../common/manifest.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts"; +import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; @@ -10,6 +11,7 @@ import { BuiltinSettings } from "../web/types.ts"; import { JWTIssuer } from "./crypto.ts"; import { gitIgnoreCompiler } from "./deps.ts"; import { ServerSystem } from "./server_system.ts"; +import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts"; import { ShellBackend } from "./shell_backend.ts"; import { determineStorageBackend } from "./storage_backend.ts"; @@ -21,8 +23,10 @@ export type SpaceServerConfig = { // Additional API auth token authToken?: string; pagesPath: string; - syncOnly?: boolean; - clientEncryption?: boolean; + shellBackend: string; + syncOnly: boolean; + readOnly: boolean; + clientEncryption: boolean; }; export class SpaceServer { @@ -41,10 +45,11 @@ export class SpaceServer { system?: System; clientEncryption: boolean; syncOnly: boolean; + readOnly: boolean; + shellBackend: ShellBackend; constructor( config: SpaceServerConfig, - public shellBackend: ShellBackend, private plugAssetBundle: AssetBundle, private kvPrimitives: KvPrimitives, ) { @@ -53,13 +58,18 @@ export class SpaceServer { this.auth = config.auth; this.authToken = config.authToken; this.clientEncryption = !!config.clientEncryption; - this.syncOnly = !!config.syncOnly; + this.syncOnly = config.syncOnly; + this.readOnly = config.readOnly; if (this.clientEncryption) { // Sync only will forced on when encryption is enabled this.syncOnly = true; } this.jwtIssuer = new JWTIssuer(kvPrimitives); + + this.shellBackend = config.readOnly + ? new NotSupportedShell() // No shell for read only mode + : determineShellBackend(config); } async init() { @@ -81,6 +91,10 @@ export class SpaceServer { }, ); + if (this.readOnly) { + this.spacePrimitives = new ReadOnlySpacePrimitives(this.spacePrimitives); + } + // system = undefined in databaseless mode (no PlugOS instance on the server and no DB) if (!this.syncOnly) { // Enable server-side processing @@ -88,6 +102,7 @@ export class SpaceServer { this.spacePrimitives, this.kvPrimitives, this.shellBackend, + this.readOnly, ); this.serverSystem = serverSystem; } diff --git a/server/server_system.ts b/server/server_system.ts index 1f4bc376..2c43b204 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -11,10 +11,9 @@ import { eventSyscalls } from "../plugos/syscalls/event.ts"; import { mqSyscalls } from "../plugos/syscalls/mq.ts"; import { System } from "../plugos/system.ts"; import { Space } from "../web/space.ts"; -import { debugSyscalls } from "../web/syscalls/debug.ts"; import { markdownSyscalls } from "../common/syscalls/markdown.ts"; -import { spaceSyscalls } from "./syscalls/space.ts"; -import { systemSyscalls } from "../web/syscalls/system.ts"; +import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts"; +import { systemSyscalls } from "../common/syscalls/system.ts"; import { yamlSyscalls } from "../common/syscalls/yaml.ts"; import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts"; import { shellSyscalls } from "./syscalls/shell.ts"; @@ -22,7 +21,10 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts"; import { Plug } from "../plugos/plug.ts"; import { DataStore } from "../plugos/lib/datastore.ts"; -import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts"; +import { + dataStoreReadSyscalls, + dataStoreWriteSyscalls, +} from "../plugos/syscalls/datastore.ts"; import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts"; import { languageSyscalls } from "../common/syscalls/language.ts"; import { handlebarsSyscalls } from "../common/syscalls/handlebars.ts"; @@ -65,6 +67,7 @@ export class ServerSystem { private baseSpacePrimitives: SpacePrimitives, readonly kvPrimitives: KvPrimitives, private shellBackend: ShellBackend, + private readOnlyMode: boolean, ) { } @@ -123,29 +126,37 @@ export class ServerSystem { this.system.registerSyscalls( [], eventSyscalls(eventHook), - spaceSyscalls(space), + spaceReadSyscalls(space), assetSyscalls(this.system), yamlSyscalls(), - systemSyscalls(this.system), + systemSyscalls(this.system, this.readOnlyMode), mqSyscalls(mq), languageSyscalls(), handlebarsSyscalls(), - dataStoreSyscalls(this.ds), - debugSyscalls(), + dataStoreReadSyscalls(this.ds), codeWidgetSyscalls(codeWidgetHook), markdownSyscalls(), ); - // Syscalls that require some additional permissions - this.system.registerSyscalls( - ["fetch"], - sandboxFetchSyscalls(), - ); + if (!this.readOnlyMode) { + // Write mode only + this.system.registerSyscalls( + [], + spaceWriteSyscalls(space), + dataStoreWriteSyscalls(this.ds), + ); - this.system.registerSyscalls( - ["shell"], - shellSyscalls(this.shellBackend), - ); + // Syscalls that require some additional permissions + this.system.registerSyscalls( + ["fetch"], + sandboxFetchSyscalls(), + ); + + this.system.registerSyscalls( + ["shell"], + shellSyscalls(this.shellBackend), + ); + } await this.loadPlugs(); diff --git a/server/shell_backend.ts b/server/shell_backend.ts index 1ab37b71..5a71861f 100644 --- a/server/shell_backend.ts +++ b/server/shell_backend.ts @@ -1,3 +1,4 @@ +import type { SpaceServerConfig } from "./instance.ts"; import { ShellRequest, ShellResponse } from "./rpc.ts"; /** @@ -5,11 +6,13 @@ import { ShellRequest, ShellResponse } from "./rpc.ts"; * - SB_SHELL_BACKEND: "local" or "off" */ -export function determineShellBackend(path: string): ShellBackend { +export function determineShellBackend( + spaceServerConfig: SpaceServerConfig, +): ShellBackend { const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local"; switch (backendConfig) { case "local": - return new LocalShell(path); + return new LocalShell(spaceServerConfig.pagesPath); default: console.info( "Running in shellless mode, meaning shell commands are disabled", @@ -22,7 +25,7 @@ export interface ShellBackend { handle(shellRequest: ShellRequest): Promise; } -class NotSupportedShell implements ShellBackend { +export class NotSupportedShell implements ShellBackend { handle(): Promise { return Promise.resolve({ stdout: "", @@ -32,7 +35,7 @@ class NotSupportedShell implements ShellBackend { } } -class LocalShell implements ShellBackend { +export class LocalShell implements ShellBackend { constructor(private cwd: string) { } diff --git a/server/syscalls/space.ts b/server/syscalls/space.ts index 97818094..bc40a8b0 100644 --- a/server/syscalls/space.ts +++ b/server/syscalls/space.ts @@ -5,7 +5,7 @@ import type { Space } from "../../web/space.ts"; /** * Almost the same as web/syscalls/space.ts except leaving out client-specific stuff */ -export function spaceSyscalls(space: Space): SysCallMapping { +export function spaceReadSyscalls(space: Space): SysCallMapping { return { "space.listPages": (): Promise => { return space.fetchPageList(); @@ -16,16 +16,6 @@ export function spaceSyscalls(space: Space): SysCallMapping { "space.getPageMeta": (_ctx, name: string): Promise => { return space.getPageMeta(name); }, - "space.writePage": ( - _ctx, - name: string, - text: string, - ): Promise => { - return space.writePage(name, text); - }, - "space.deletePage": async (_ctx, name: string) => { - await space.deletePage(name); - }, "space.listPlugs": (): Promise => { return space.listPlugs(); }, @@ -41,16 +31,6 @@ export function spaceSyscalls(space: Space): SysCallMapping { ): Promise => { return await space.getAttachmentMeta(name); }, - "space.writeAttachment": ( - _ctx, - name: string, - data: Uint8Array, - ): Promise => { - return space.writeAttachment(name, data); - }, - "space.deleteAttachment": async (_ctx, name: string) => { - await space.deleteAttachment(name); - }, // FS "space.listFiles": (): Promise => { @@ -62,6 +42,31 @@ export function spaceSyscalls(space: Space): SysCallMapping { "space.readFile": async (_ctx, name: string): Promise => { return (await space.spacePrimitives.readFile(name)).data; }, + }; +} + +export function spaceWriteSyscalls(space: Space): SysCallMapping { + return { + "space.writePage": ( + _ctx, + name: string, + text: string, + ): Promise => { + return space.writePage(name, text); + }, + "space.deletePage": async (_ctx, name: string) => { + await space.deletePage(name); + }, + "space.writeAttachment": ( + _ctx, + name: string, + data: Uint8Array, + ): Promise => { + return space.writeAttachment(name, data); + }, + "space.deleteAttachment": async (_ctx, name: string) => { + await space.deleteAttachment(name); + }, "space.writeFile": ( _ctx, name: string, diff --git a/web/boot.ts b/web/boot.ts index 1e3694db..52a4f480 100644 --- a/web/boot.ts +++ b/web/boot.ts @@ -10,9 +10,14 @@ safeRun(async () => { syncMode ? "in Sync Mode" : "in Online Mode", ); + if (window.silverBulletConfig.readOnly) { + console.log("Running in read-only mode"); + } + const client = new Client( document.getElementById("sb-root")!, syncMode, + window.silverBulletConfig.readOnly, ); window.client = client; await client.init(); diff --git a/web/client.ts b/web/client.ts index e646a331..14d461e0 100644 --- a/web/client.ts +++ b/web/client.ts @@ -59,6 +59,8 @@ import { LimitedMap } from "$sb/lib/limited_map.ts"; import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts"; import { buildQueryFunctions } from "../common/query_functions.ts"; import { PageRef } from "$sb/lib/page.ts"; +import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts"; +import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; const autoSaveInterval = 1000; @@ -69,6 +71,7 @@ declare global { silverBulletConfig: { spaceFolderPath: string; syncOnly: boolean; + readOnly: boolean; clientEncryption: boolean; }; client: Client; @@ -87,7 +90,6 @@ export class Client { private dbPrefix: string; plugSpaceRemotePrimitives!: PlugSpacePrimitives; - // localSpacePrimitives!: FilteredSpacePrimitives; httpSpacePrimitives!: HttpSpacePrimitives; space!: Space; @@ -109,7 +111,7 @@ export class Client { ui!: MainUI; stateDataStore!: DataStore; - spaceDataStore!: DataStore; + spaceKV?: KvPrimitives; mq!: DataStoreMQ; // Used by the "wiki link" highlighter to check if a page exists @@ -118,7 +120,8 @@ export class Client { constructor( private parent: Element, - public syncMode = false, + public syncMode: boolean, + private readOnlyMode: boolean, ) { if (!syncMode) { this.fullSyncCompleted = true; @@ -159,6 +162,7 @@ export class Client { this.mq, this.stateDataStore, this.eventHook, + window.silverBulletConfig.readOnly, ); const localSpacePrimitives = await this.initSpace(); @@ -518,6 +522,12 @@ export class Client { } } + if (this.readOnlyMode) { + remoteSpacePrimitives = new ReadOnlySpacePrimitives( + remoteSpacePrimitives, + ); + } + this.plugSpaceRemotePrimitives = new PlugSpacePrimitives( remoteSpacePrimitives, this.system.namespaceHook, @@ -535,6 +545,8 @@ export class Client { ); await spaceKvPrimitives.init(); + this.spaceKV = spaceKvPrimitives; + localSpacePrimitives = new FilteredSpacePrimitives( new EventedSpacePrimitives( // Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet diff --git a/web/client_system.ts b/web/client_system.ts index 74c0179c..91ed96e1 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -17,16 +17,19 @@ import { editorSyscalls } from "./syscalls/editor.ts"; import { sandboxFetchSyscalls } from "./syscalls/fetch.ts"; import { markdownSyscalls } from "../common/syscalls/markdown.ts"; import { shellSyscalls } from "./syscalls/shell.ts"; -import { spaceSyscalls } from "./syscalls/space.ts"; +import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts"; import { syncSyscalls } from "./syscalls/sync.ts"; -import { systemSyscalls } from "./syscalls/system.ts"; +import { systemSyscalls } from "../common/syscalls/system.ts"; import { yamlSyscalls } from "../common/syscalls/yaml.ts"; import { Space } from "./space.ts"; import { MQHook } from "../plugos/hooks/mq.ts"; import { mqSyscalls } from "../plugos/syscalls/mq.ts"; import { mqProxySyscalls } from "./syscalls/mq.proxy.ts"; import { dataStoreProxySyscalls } from "./syscalls/datastore.proxy.ts"; -import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts"; +import { + dataStoreReadSyscalls, + dataStoreWriteSyscalls, +} from "../plugos/syscalls/datastore.ts"; import { DataStore } from "../plugos/lib/datastore.ts"; import { MessageQueue } from "../plugos/lib/mq.ts"; import { languageSyscalls } from "../common/syscalls/language.ts"; @@ -54,6 +57,7 @@ export class ClientSystem { private mq: MessageQueue, private ds: DataStore, private eventHook: EventHook, + private readOnlyMode: boolean, ) { // Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid) this.system = new System( @@ -153,8 +157,8 @@ export class ClientSystem { [], eventSyscalls(this.eventHook), editorSyscalls(this.client), - spaceSyscalls(this.client), - systemSyscalls(this.system, this.client), + spaceReadSyscalls(this.client), + systemSyscalls(this.system, false, this.client), markdownSyscalls(), assetSyscalls(this.system), yamlSyscalls(), @@ -167,24 +171,31 @@ export class ClientSystem { ? mqSyscalls(this.mq) // In non-sync mode proxy to server : mqProxySyscalls(this.client), - this.client.syncMode - ? dataStoreSyscalls(this.ds) - : dataStoreProxySyscalls(this.client), - debugSyscalls(), + ...this.client.syncMode + ? [dataStoreReadSyscalls(this.ds), dataStoreWriteSyscalls(this.ds)] + : [dataStoreProxySyscalls(this.client)], + debugSyscalls(this.client), syncSyscalls(this.client), clientStoreSyscalls(this.ds), ); - // Syscalls that require some additional permissions - this.system.registerSyscalls( - ["fetch"], - sandboxFetchSyscalls(this.client), - ); + if (!this.readOnlyMode) { + // Write syscalls + this.system.registerSyscalls( + [], + spaceWriteSyscalls(this.client), + ); + // Syscalls that require some additional permissions + this.system.registerSyscalls( + ["fetch"], + sandboxFetchSyscalls(this.client), + ); - this.system.registerSyscalls( - ["shell"], - shellSyscalls(this.client), - ); + this.system.registerSyscalls( + ["shell"], + shellSyscalls(this.client), + ); + } } async reloadPlugsFromSpace(space: Space) { diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index 8bc7f580..0143da31 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -34,7 +34,9 @@ export class MainUI { console.log("Closing search panel"); closeSearchPanel(client.editorView); return; - } else if (target.closest(".cm-content")) { + } else if ( + target.className === "cm-textfield" || target.closest(".cm-content") + ) { // In some cm element, let's back out return; } diff --git a/web/hooks/command.ts b/web/hooks/command.ts index 1ee4c5b8..bf77c36d 100644 --- a/web/hooks/command.ts +++ b/web/hooks/command.ts @@ -22,6 +22,7 @@ export type CommandDef = { mac?: string; hide?: boolean; + requireMode?: "rw" | "ro"; }; export type AppCommand = { @@ -58,6 +59,10 @@ export class CommandHook extends EventEmitter continue; } const cmd = functionDef.command; + if (cmd.requireMode === "rw" && window.silverBulletConfig.readOnly) { + // Bit hacky, but don't expose commands that require write mode in read-only mode + continue; + } this.editorCommands.set(cmd.name, { command: cmd, run: (args?: string[]) => { diff --git a/web/index.html b/web/index.html index 787463c3..543595e6 100644 --- a/web/index.html +++ b/web/index.html @@ -39,6 +39,7 @@ // These {{VARIABLES}} are replaced by http_server.ts spaceFolderPath: "{{SPACE_PATH}}", syncOnly: "{{SYNC_ONLY}}" === "true", + readOnly: "{{READ_ONLY}}" === "true", clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true", }; // But in case these variables aren't replaced by the server, fall back sync only mode @@ -46,6 +47,7 @@ window.silverBulletConfig = { spaceFolderPath: "", syncOnly: true, + readOnly: false, clientEncryption: false, }; } diff --git a/web/logout.html b/web/logout.html deleted file mode 100644 index 767382b5..00000000 --- a/web/logout.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - Reset SilverBullet - - - - - -
-

Logout

-
- - - - - - - \ No newline at end of file diff --git a/web/service_worker.ts b/web/service_worker.ts index 05e39c6b..4694d00d 100644 --- a/web/service_worker.ts +++ b/web/service_worker.ts @@ -7,7 +7,6 @@ const CACHE_NAME = "{{CACHE_NAME}}_{{CONFIG_HASH}}"; const precacheFiles = Object.fromEntries([ "/", - "/.client/logout.html", "/.client/client.js", "/.client/favicon.png", "/.client/iAWriterMonoS-Bold.woff2", diff --git a/web/syscalls/datastore.proxy.ts b/web/syscalls/datastore.proxy.ts index ec61cf3f..a11acd7e 100644 --- a/web/syscalls/datastore.proxy.ts +++ b/web/syscalls/datastore.proxy.ts @@ -1,8 +1,6 @@ -import { KvQuery } from "$sb/types.ts"; -import { LimitedMap } from "../../plug-api/lib/limited_map.ts"; import type { SysCallMapping } from "../../plugos/system.ts"; import type { Client } from "../client.ts"; -import { proxySyscall, proxySyscalls } from "./util.ts"; +import { proxySyscalls } from "./util.ts"; export function dataStoreProxySyscalls(client: Client): SysCallMapping { return proxySyscalls(client, [ diff --git a/web/syscalls/debug.ts b/web/syscalls/debug.ts index 353bf4c8..5b3265d9 100644 --- a/web/syscalls/debug.ts +++ b/web/syscalls/debug.ts @@ -1,6 +1,8 @@ +import { KvKey } from "$sb/types.ts"; import type { SysCallMapping } from "../../plugos/system.ts"; +import { Client } from "../client.ts"; -export function debugSyscalls(): SysCallMapping { +export function debugSyscalls(client: Client): SysCallMapping { return { "debug.resetClient": async () => { if (navigator.serviceWorker) { @@ -44,5 +46,20 @@ export function debugSyscalls(): SysCallMapping { alert("Reset complete, now reloading the page..."); location.reload(); }, + "debug.cleanup": async () => { + if (client.spaceKV) { + console.log("Wiping the entire space KV store"); + // In sync mode, we can just delete the whole space + const allKeys: KvKey[] = []; + for await (const { key } of client.spaceKV.query({})) { + allKeys.push(key); + } + await client.spaceKV.batchDelete(allKeys); + } + localStorage.clear(); + console.log("Wiping the entire state KV store"); + await client.stateDataStore.queryDelete({}); + console.log("Done"); + }, }; } diff --git a/web/syscalls/space.ts b/web/syscalls/space.ts index 7e5b5abc..1d63d33f 100644 --- a/web/syscalls/space.ts +++ b/web/syscalls/space.ts @@ -2,7 +2,7 @@ import { Client } from "../client.ts"; import { SysCallMapping } from "../../plugos/system.ts"; import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts"; -export function spaceSyscalls(editor: Client): SysCallMapping { +export function spaceReadSyscalls(editor: Client): SysCallMapping { return { "space.listPages": (): Promise => { return editor.space.fetchPageList(); @@ -13,6 +13,36 @@ export function spaceSyscalls(editor: Client): SysCallMapping { "space.getPageMeta": (_ctx, name: string): Promise => { return editor.space.getPageMeta(name); }, + "space.listPlugs": (): Promise => { + return editor.space.listPlugs(); + }, + "space.listAttachments": async (): Promise => { + return await editor.space.fetchAttachmentList(); + }, + "space.readAttachment": async (_ctx, name: string): Promise => { + return (await editor.space.readAttachment(name)).data; + }, + "space.getAttachmentMeta": async ( + _ctx, + name: string, + ): Promise => { + return await editor.space.getAttachmentMeta(name); + }, + // FS + "space.listFiles": (): Promise => { + return editor.space.spacePrimitives.fetchFileList(); + }, + "space.getFileMeta": (_ctx, name: string): Promise => { + return editor.space.spacePrimitives.getFileMeta(name); + }, + "space.readFile": async (_ctx, name: string): Promise => { + return (await editor.space.spacePrimitives.readFile(name)).data; + }, + }; +} + +export function spaceWriteSyscalls(editor: Client): SysCallMapping { + return { "space.writePage": ( _ctx, name: string, @@ -30,21 +60,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping { console.log("Deleting page"); await editor.space.deletePage(name); }, - "space.listPlugs": (): Promise => { - return editor.space.listPlugs(); - }, - "space.listAttachments": async (): Promise => { - return await editor.space.fetchAttachmentList(); - }, - "space.readAttachment": async (_ctx, name: string): Promise => { - return (await editor.space.readAttachment(name)).data; - }, - "space.getAttachmentMeta": async ( - _ctx, - name: string, - ): Promise => { - return await editor.space.getAttachmentMeta(name); - }, "space.writeAttachment": ( _ctx, name: string, @@ -55,17 +70,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping { "space.deleteAttachment": async (_ctx, name: string) => { await editor.space.deleteAttachment(name); }, - - // FS - "space.listFiles": (): Promise => { - return editor.space.spacePrimitives.fetchFileList(); - }, - "space.getFileMeta": (_ctx, name: string): Promise => { - return editor.space.spacePrimitives.getFileMeta(name); - }, - "space.readFile": async (_ctx, name: string): Promise => { - return (await editor.space.spacePrimitives.readFile(name)).data; - }, "space.writeFile": ( _ctx, name: string, diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index b5fe513b..5d12a22c 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -12,6 +12,8 @@ _The changes below are not yet released “properly”. To them out early, check * Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this. * Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the type of link formats that SilverBullet now supports. * New {[Task: Remove Completed]} command to remove all completed tasks from a page +* **Read-only mode** (experimental) is here, see [[Install/Configuration#Run mode]] on how to enable it. Allowing you expose your space to the outside world in all its glory, but without allowing anybody to edit anything. This should be fairly locked down and secure, but back up your stuff! +* New {[Clear Local Storage & Logout]} command to wipe out any locally synced data (and log you out if you use [[Authentication]]). * Bug fixes: * Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links. * Right-clicking links now opens browser native context menu again diff --git a/website/Install/Configuration.md b/website/Install/Configuration.md index 7095d2eb..1f1da002 100644 --- a/website/Install/Configuration.md +++ b/website/Install/Configuration.md @@ -1,21 +1,18 @@ SilverBullet is primarily configured via environment variables. This page gives a comprehensive overview of all configuration options. You can set these ad-hoc when running the SilverBullet server, or e.g. in your [[Install/Docker|docker-compose file]]. # Network -$network Note: these options are primarily useful for [[Install/Deno]] deployments, not so much for [[Install/Docker]]. * `SB_HOSTNAME`: Set to the hostname to bind to (defaults to `127.0.0.0`, set to `0.0.0.0` to accept outside connections for the local deno setup, defaults to `0.0.0.0` for docker) * `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000` # Authentication -$authentication SilverBullet supports basic authentication for a single user. * `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”. * `SB_AUTH_TOKEN`: Enables `Authorization: Bearer ` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends). # Storage -$storage SilverBullet supports multiple storage backends for keeping your [[Spaces]] content. ## Disk storage @@ -26,7 +23,8 @@ This is the default and simplest backend to use: a folder on disk. It is configu ## AWS S3 bucket storage It is also possible to use an S3 bucket as storage. For this, you need to create a bucket, create an IAM user and configure access to it appropriately. -Since S3 doesn’t support an efficient way to store custom metadata, this mode does require a [[$database]] configuration (see below) to keep all file metadata. +Since S3 doesn’t support an efficient way to store custom metadata, this mode does require a [[ +]] configuration (see below) to keep all file metadata. S3 is configured as follows: @@ -38,13 +36,13 @@ S3 is configured as follows: * `AWS_REGION`: e.g. `eu-central-1` ## Database storage -It is also possible to store space content in the [[$database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size. +It is also possible to store space content in the [[#Database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size. This mode is configured as follows: * `SB_FOLDER`: set to `db://` -The database configured via [[$database]] will be used. +The database configured via [[#Database]] will be used. ## HTTP storage While not particularly useful stand-alone (primarily for [[Sync]]), it is possible to store space content on _another_ SilverBullet installation via its [[API]]. @@ -52,10 +50,9 @@ While not particularly useful stand-alone (primarily for [[Sync]]), it is possib This mode is configured as follows: * `SB_FOLDER`: set to the URL of the other SilverBullet server, e.g. `https://mynotes.mydomain.com` -* `SB_AUTH_TOKEN`: matching the authorization token (configured via [[$authentication]] on the other end) to use for authorization. +* `SB_AUTH_TOKEN`: matching the authorization token (configured via [[#Authentication]] on the other end) to use for authorization. # Database -$database SilverBullet requires a database backend to (potentially) keep various types of data: * Indexes for e.g. [[Objects]] @@ -80,16 +77,13 @@ The in-memory database is only useful for testing. * `SB_DB_BACKEND`: `memory` # Run mode -$runmode - * `SB_SYNC_ONLY`: If you want to run SilverBullet in a mode where the server purely functions as a simple file store and doesn’t index or process content on the server, you can do so by setting this environment variable to `true`. As a result, the client will always run in the Sync [[Client Modes|client mode]]. +* `SB_READ_ONLY` (==Experimental==): If you want to run the SilverBullet client and server in read-only mode (you get the full SilverBullet client, but all edit functionality and commands are disabled), you can do this by setting this environment variable to `true`. Upon the server start a full space index will happen, after which all write operations will be disabled. # Security -$security - SilverBullet enables plugs to run shell commands. This is used by e.g. the [[Plugs/Git]] plug to perform git commands. This is potentially unsafe. If you don’t need this, you can disable this functionality: -* `SB_SHELL_BACKEND`: Enable/disable running of shell commands from plugs, defaults to `local` (enabled), set to `off` to disable. It is only enabled when using a local folder for [[$storage]]. +* `SB_SHELL_BACKEND`: Enable/disable running of shell commands from plugs, defaults to `local` (enabled), set to `off` to disable. It is only enabled when using a local folder for [[#Storage]]. # Docker diff --git a/website/Sync.md b/website/Sync.md index df74c842..188fdd1c 100644 --- a/website/Sync.md +++ b/website/Sync.md @@ -1,11 +1,11 @@ -The SilverBullet CLI has a `sync` command that can be used to synchronize local as well as remote [[Spaces]]. This can be useful when migrating between different [[Install/Configuration$storage|storage implementations]]. It can also be used to back up content elsewhere. Under the hood, this sync mechanism uses the exact same sync engine used for the Sync [[Client Modes]]. +The SilverBullet CLI has a `sync` command that can be used to synchronize local as well as remote [[Spaces]]. This can be useful when migrating between different [[Install/Configuration#Storage|storage implementations]]. It can also be used to back up content elsewhere. Under the hood, this sync mechanism uses the exact same sync engine used for the Sync [[Client Modes]]. # Use cases * **Migration**: you hosted SilverBullet on your local device until now, but have since set up an instance via [[Install/Deno Deploy]] and want to migrate your content there. * **Backup**: you host SilverBullet on a remote server, but would like to make backups elsewhere from time to time. # Setup -To use `silverbullet sync` you need a [[Install/Local$deno|local deno installation of SilverBullet]]. +To use `silverbullet sync` you need a [[Install/Deno|local deno installation of SilverBullet]]. # General use To perform a sync between two locations: @@ -14,7 +14,7 @@ To perform a sync between two locations: silverbullet sync --snapshot snapshot.json ``` -Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration$storage]] configuration. +Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration#Storage]] configuration. The `--snapshot` argument is optional; when set, it will read/write a snapshot to the given location. This snapshot will be used to speed up future synchronizations. @@ -25,7 +25,7 @@ silverbullet sync --snapshot snapshot.json testspace testspace2 ``` # Migrate -To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration$authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`: +To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration#Authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`: ```shell SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com