import { HttpServer } from "../server/http_server.ts"; // To ship silverbullet as a single binary, we're importing all assets (client and plugs) as JSON blobs import clientAssetBundle from "../dist/client_asset_bundle.json" with { type: "json", }; import plugAssetBundle from "../dist/plug_asset_bundle.json" with { type: "json", }; import { AssetBundle, type AssetJson } from "../lib/asset_bundle/bundle.ts"; import { resolve } from "@std/path"; import { sleep } from "../lib/async.ts"; import { MemoryKvPrimitives } from "../lib/data/memory_kv_primitives.ts"; import { isValidName } from "@silverbulletmd/silverbullet/lib/ref"; export type AuthOptions = { authToken?: string; user: string; pass: string; lockoutTime: number; lockoutLimit: number; }; export async function serveCommand( options: { hostname?: string; port?: number; user?: string; auth?: string; }, folder?: string, ) { const hostname = options.hostname || Deno.env.get("SB_HOSTNAME") || "127.0.0.1"; const port = options.port || (Deno.env.get("SB_PORT") && +Deno.env.get("SB_PORT")!) || 3000; const readOnly = !!Deno.env.get("SB_READ_ONLY"); if (readOnly) { console.info("Starting in read-only mode."); } const indexPage = Deno.env.get("SB_INDEX_PAGE") || "index"; if (!isValidName(indexPage)) { console.error( "SB_INDEX_PAGE has to be a valid page name.", ); Deno.exit(1); } folder = folder || Deno.env.get("SB_FOLDER"); if (!folder) { console.error( "No folder specified. Please pass a folder as an argument or set SB_FOLDER environment variable.", ); Deno.exit(1); } // This database is only used to store some auth based temporary values let dbFile = Deno.env.get("SB_KV_DB") || ".silverbullet.db.json"; dbFile = resolve(folder, dbFile); console.info( `Storing database in ${dbFile}.`, ); // The only use case of this KV is to store auth-related stuff (keys, hashes) const baseKvPrimitives = new MemoryKvPrimitives(dbFile, { throttleMs: 100, }); await baseKvPrimitives.init(); console.info("Starting SilverBullet binding to", `${hostname}:${port}`); if (hostname === "127.0.0.1") { console.info( `SilverBullet will only be available locally, to allow outside connections, pass -L0.0.0.0 as a flag, and put a TLS terminator on top.`, ); } let hostUrlPrefix = Deno.env.get("SB_URL_PREFIX"); if (hostUrlPrefix) { // Normalize the host URL prefix (if set) if (!hostUrlPrefix.startsWith("/")) { hostUrlPrefix = "/" + hostUrlPrefix; } if (hostUrlPrefix.endsWith("/")) { hostUrlPrefix = hostUrlPrefix.replace(/\/*$/, ""); } if (hostUrlPrefix !== "") { console.log(`Host URL Prefix: ${hostUrlPrefix}`); } else { hostUrlPrefix = undefined; } } // Built-in authentication setup const userAuth = options.user || Deno.env.get("SB_USER"); let userCredentials: AuthOptions | undefined; if (userAuth) { const [user, pass] = userAuth.split(":"); userCredentials = { user, pass, authToken: Deno.env.get("SB_AUTH_TOKEN"), // Default lockout: 10 failed login attempts in 1 minute lockoutLimit: Deno.env.get("SB_LOCKOUT_LIMIT") ? Number(Deno.env.get("SB_LOCKOUT_LIMIT")) : 10, lockoutTime: Deno.env.get("SB_LOCKOUT_TIME") ? Number(Deno.env.get("SB_LOCKOUT_TIME")) : 60, }; console.log( `User authentication enabled for user "${user}" with lockout limit ${userCredentials.lockoutLimit} and lockout time ${userCredentials.lockoutTime}s`, ); } const shellBackend = Deno.env.get("SB_SHELL_BACKEND") || "local"; const shellCommandWhiteList = Deno.env.get("SB_SHELL_WHITELIST")?.split(" "); const spaceIgnore = Deno.env.get("SB_SPACE_IGNORE"); if (spaceIgnore) { console.log(`Ignoring files matching: ${spaceIgnore}`); } // All plug code bundled into a JSON blob const plugAssets = new AssetBundle(plugAssetBundle as AssetJson); // All client files bundled into a JSON blob const clientAssets = new AssetBundle(clientAssetBundle as AssetJson); // Manifest configuration buildPWAManifest(hostUrlPrefix, clientAssets); // Build the web server const httpServer = new HttpServer( { hostname, port, hostUrlPrefix, indexPage, spaceIgnore, auth: userCredentials, readOnly, shellBackend, pagesPath: folder, shellCommandWhiteList, }, clientAssets, plugAssets, baseKvPrimitives, ); // And kick it off await httpServer.start(); // Wait in an infinite loop (to keep the HTTP server running, only cancelable via Ctrl+C or other signal) while (true) { await sleep(10000); } } /** * Overrides SilverBullet's manifest.json dynamically based on some (optional) environment variables */ function buildPWAManifest( hostUrlPrefix: string | undefined, clientAssets: AssetBundle, ) { const manifestName = Deno.env.get("SB_NAME"); const manifestDescription = Deno.env.get("SB_DESCRIPTION"); // Parse the default manifest.json const manifestData = JSON.parse( clientAssets.readTextFileSync(".client/manifest.json"), ); if (manifestName) { manifestData.name = manifestData.short_name = manifestName; } if (manifestDescription) { manifestData.description = manifestDescription; } if (hostUrlPrefix) { for (const icon of manifestData.icons) { if (icon.src) icon.src = hostUrlPrefix + icon.src; } manifestData.start_url = hostUrlPrefix + manifestData.start_url; manifestData.scope = hostUrlPrefix + manifestData.scope; } // Write it back into the asset bundle to be served by the HTTP server clientAssets.writeTextFileSync( ".client/manifest.json", "application/json", JSON.stringify(manifestData), ); }