Smallweb 0.13 - Private Apps, Terminal, WebDAV, and CLI entrypoints
by Achille Lacoin
6 min read
Hey, It's been a while since the last release of smallweb! I've been taking a break for the month of August, but I'm back with the biggest smallweb release yet, including:
- Protecting private apps with authentication
- Built-in terminal and WebDAV services
- CLI entrypoints
Private Apps
You can now easily guard your smallweb apps behind an authentication mechanism.
-
Add the some field to the global config (located at
~/.config/smallweb/config.json
){ "email": "[email protected]" }
You can easily generate a new token with the
smallweb token
command. -
Set the
private
field to true in your app config.// ~/smallweb/example-app/smallweb.json { "private": true }
From now on, you will need to authenticate when accessing the app.
The auth mechanism leverage lastlogin.io to provide a simple and secure authentication mechanism. I might host my own instance of lastlogin in the future, but for now, I choosed to rely on the public instance.
Alternatively, you can provide a token in the Authorization
header.
curl https://example-app.localhost --header "Authorization: Bearer <token>"
Or alternatively:
curl https://<token>@example-app.localhost
To get a smallweb token, you can use the smallweb token create
command.
$ smallweb token create --description "My secret token"
Pj0e8hlwTolo7IoPo6ksA
Built-in Admin Apps
This new authentication mechanism enabled me to distribute admin services as part of smallweb. In this release, there are two admin apps available:
webdav.<your-domain>
: A WebDAV server to manage your filescli.<your-domain>
: Allowing you to access the cli from your browser
But i'm considering adding more in the future (feel free to suggest some!).
cli service
You can now access the cli from your browser! This is a great way to manage a remote smallweb instance without having to ssh into it.
If you go to cli.<your-domain>
, you will be prompted to authenticate, then be able to run any command you want.
The cli is protected behind the same authentication mechanism as the rest of the apps.
smallweb ls --json
-> curl -X POST 'https://cli.<your-domain>/ls?json' --header "Authorization: Bearer my-secret-token"
In a future release, the cli service might include a terminal emulator using xterm.js.
webdav service
I've been rediscovering the power of WebDAV recently, and I wanted to make it easy to pair it with smallweb. The built-in webdav servers allows you to manage/backup your smallweb folder using any webdav client.
It means that smallweb can now be integrated with:
- All major operating systems (Windows, MacOS, Linux)
- Web based file managers (ex: https://www.filestash.app/)
To authenticate, you can either:
- Use bearer token authentication
- Use basic authentication, and provide the token as the username
Currently the WebDAV server does not comes with any UI, as I was not able to find a webdav client both easily embeddable and good-looking. If you have any suggestion, feel free to reach out!
Having a webdav server also allows you to easily backup your smallweb apps using a tool like rclone
.
CLI entrypoints
Smallweb apps are required to export an object, as their default export, that contain's a fetch
method to handle incoming requests.
// ~/smallweb/example-app/main.ts
export default {
fetch: async (request: Request) => {
return new Response('Hello, World!');
}
}
From now on, you can also register a run
method to handle cli commands.
// ~/smallweb/example-app/main.ts
export default {
fetch: async (request: Request) => {
return new Response('Hello, World!');
},
run: async (args: string[]) => {
console.log(`Hello, ${args[0]}!`);
}
}
Once you have registered a run
method, you can run it with the smallweb run
command.
$ smallweb run example-app pomdtr
Hello, pomdtr!
Smallweb commands run with the same permissions as the app itself. You can access stdin/stdout/stderr, not spawn subprocesses.
You can also access the newly created cli from the internal cli.<your-domain>
service, either from your browser or using curl.
curl -X POST https://cli.<your-domain>/run/example-app/pomdtr -H "Authorization: Bearer my-secret-token"
These cli entrypoints are also leveraged for cron tasks (which schema has been updated in this release).
// ~/smallweb/example-app/smallweb.json
{
"crons": [
{
"name": "say-hello-to-pomdtr",
"description": "Say hello to pomdtr every day",
"schedule": "0 0 * * *",
"args": ["pomdtr"]
}
]
}
Updated permissions
You used to be able to customize the permissions of a smallweb app using the permissions
field in the app config. There was a big issue with this approach: if an app had write access to the filesystem, it could edit it's own permissions, and escalate its privileges.
Since I envision smallweb as a platform where user can run remote or AI generated code, I wanted to make sure that the permissions of an app are as restricted as possible.
In this release, app permissions are now longer customizable. Instead, each smallweb app is allowed to:
- read and write in its own directory
- access the network using the fetch API
- access the env variable defined either in the global config or in the
.env
file
If you want to give you app additional permissions, you can provide it with a token has an environment variable.
# ~/smallweb/example-app.env
SMALLWEB_TOKEN=my-secret-token
Then, you'll be able to access the cli from your app:
const token = Deno.env.get("SMALLWEB_TOKEN");
const resp = await fetch("https://cli.<your-domain>/ls?json", {
headers: {
method: "POST",
Authorization: `Bearer ${token}`,
}
});
const apps = await resp.json();
Or build your own smallweb editor using a WebDAV client!
smallweb edit
command
Use the smallweb edit [app]
command to open an app in your default editor.
Of course, it integrates with the cli.<your-domain>
service, allowing you to edit your apps from your browser.
smallweb types
command
With the addition of the run
method, it can become a bit cumbersome to check that your default export is compatible with the expected smallweb schema.
I added a new smallweb types
command to help you generate types hint.
To add the smallweb types to your project, run:
smallweb types > smallweb.d.ts
You can then reference them in your project:
export default {
fetch: async (request) => {
return new Response('Hello, World!');
},
run: async (args) => {
console.log(`Hello, ${args[0]}!`);
}
} satisfies Smallweb.App;
Simplified Global Configuration
The global configuration now has the following schema:
// ~/.config/smallweb/config.json
{
"host": "127.0.0.1",
"port": 8080,
"email": "[email protected]",
"domain": "smallweb.run",
"dir": "~/smallweb"
}
All of these fields can also be provided using environment variables (ex: host
can be provided using the SMALLWEB_HOST
environment variable).
As you can see, the complex domain rooting from previous versions has been dropped. A smallweb instance maps a single wildcard domain to a single directory. If you want to manage multiple domains, feel free to run multiple instances of smallweb!
ex: I host two instances of smallweb on my hetzner VM (smallweb.run
and pomdtr.me
). To do so, I created two users (smallweb.run
and pomdtr.me
), and run smallweb as a service for each of them.
Of course, you can still attach a custom domain to a specific app by adding a CNAME
file in the app directory, containing the domain you want to attach.
Updated app config
The schema of the smallweb config was updated:
{
"private": true,
"entrypoint": "main.ts",
"root": "public",
"crons": []
}
We already detailed above:
- The addition of the
private
field - The removal of the
permissions
field - The update of the
crons
field
In addition to this:
- the
serve
field was replaced by theentrypoint
field. You can use it to specify which file exports the app object. If you don't provide it, smallweb will default tomain.[js,ts,jsx,tsx]
. - the
root
field allows you to specify the root directory of the app. Theentrypoint
file will be resolved relative to this directory.
Smallweb no longer serve static files by default. If you want to serve static files, you can use the following main.ts:
// ~/smallweb/example-app/main.ts
import { serveDir } from "jsr:@std/http/file-server";
export default {
fetch: (req: Request) => serveDir(req, {
// The root directory of the app
fsRoot: "./static",
}),
}
Or even simpler, add the following smallweb.json
file:
{
"entrypoint": "jsr:@smallweb/file-server",
"root": "static" // or omit it to serve the whole app directory
}