Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ci
on:
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # 4.0.1
with:
install_args: 'node'
- run: corepack enable
- run: yarn install
- run: yarn gen:fumadocs
- run: yarn lint
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # 4.0.1
with:
install_args: 'node'
- run: corepack enable
- run: yarn install
- run: yarn format --check
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ node_modules
.source/
dist/
.wrangler/

.dev.vars
.idea

# macOS
.DS_Store
3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
nodeLinker: node-modules

npmMinimalAgeGate: 0

preferReuse: true
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2024-2026 Seokju Na

Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
184 changes: 184 additions & 0 deletions content/docs/cli.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
title: CLI reference
description: The wvb command-line tool — pack, serve, upload, deploy, download, and the local remote.
---

The `wvb` command-line tool (package `@wvb/cli`, also installed as `webview-bundle`) packs bundles
and drives the remote update workflow. Install it as a dev dependency:

```sh
npm install -D @wvb/cli
npx wvb --help
```

Most commands read defaults from a [`wvb.config.ts`](/docs/configuration) found in the working
directory. Pass `--config <path>` to use a specific file and `--cwd <dir>` to change the working
directory.

## Authoring bundles

### `wvb pack [SRC_DIR]`

Pack a directory of web assets into a `.wvb` archive.

```sh
wvb pack ./dist
wvb pack ./dist --outfile ./app.wvb
wvb pack ./dist --ignore '*.map' --ignore 'node_modules/**'
wvb pack ./dist --header '*.html' 'cache-control' 'max-age=3600'
```

| Option | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| `SRC_DIR` | Source directory (or `pack.srcDir` in config). |
| `--outfile, -O` | Output file name. Defaults to the `package.json` name; `.wvb` is appended if missing. |
| `--outdir` | Output directory. Defaults to `.wvb`. |
| `--ignore` | Glob(s) of files to exclude (repeatable). |
| `--header, -H` | Set headers for matching files: `--header <glob> <key> <value>` (repeatable). |
| `--write` | Set `--no-write` to simulate without writing. |
| `--overwrite` | Overwrite an existing output file. Default `true`. |

### `wvb extract [FILE]`

Extract a `.wvb` archive's files back onto disk.

```sh
wvb extract ./app.wvb --outdir ./unpacked
```

| Option | Description |
| -------------- | ------------------------------------------------ |
| `FILE` | Bundle file to extract. |
| `--outdir, -O` | Destination directory. |
| `--clean` | Remove the out directory first. Default `false`. |
| `--write` | Set `--no-write` to simulate. |

### `wvb serve [FILE]`

Serve a single bundle's files over HTTP — useful for previewing a packed bundle in a browser.

```sh
wvb serve ./app.wvb # http://localhost:4312
wvb serve ./app.wvb --port 8080 --hostname 0.0.0.0
```

| Option | Description |
| ---------------- | ---------------------------------------------------- |
| `FILE` | Bundle to serve (or `serve.file` in config). |
| `--hostname, -H` | Bind hostname. Default `localhost`. (`HOSTNAME` env) |
| `--port, -P` | Port. Default `4312`. (`PORT` env) |
| `--silent` | Disable request logging. |

## Publishing & updating

These commands require `remote.*` settings in your [config](/docs/configuration). See
[Remote updates](/docs/remote-updates) for the full workflow and a local-testing walkthrough.

### `wvb upload [BUNDLE] [VERSION]`

Upload a packed bundle to the remote, optionally computing integrity, signing it, and deploying.

```sh
wvb upload # uses config defaults
wvb upload app 1.2.0 --deploy
wvb upload app 1.2.0 --deploy --channel beta
wvb upload --file ./dist/app.wvb --force
```

| Option | Description |
| ------------------- | ------------------------------------------------------------ |
| `BUNDLE`, `VERSION` | Bundle name and version (else from config / `package.json`). |
| `--file, -F` | Path to the `.wvb` to upload. |
| `--force` | Overwrite if the version already exists. |
| `--deploy` | Deploy after upload. Default `true`. |
| `--channel` | Channel to deploy to (with `--deploy`). |
| `--skip-integrity` | Don't compute an integrity hash. |
| `--skip-signature` | Don't sign the bundle. |

### `wvb deploy [BUNDLE] VERSION`

Deploy a previously uploaded version (make it the current version clients receive).

```sh
wvb deploy app 1.2.0
wvb deploy app 1.2.0 --channel beta
```

### `wvb download [BUNDLE] [VERSION]`

Download a bundle from the remote and (by default) save it to disk.

```sh
wvb download app --endpoint https://updates.example.com
wvb download app 1.2.0 --out ./bundles/app.wvb --overwrite
wvb download app --skip-write # fetch + print info only
```

| Option | Description |
| ---------------- | -------------------------------------------- |
| `--out, -O` | Output path. Defaults to `<name>.wvb`. |
| `--endpoint, -E` | Remote endpoint (else `remote.endpoint`). |
| `--channel` | Channel to download from. |
| `--write` | Set `--no-write` to skip saving. |
| `--overwrite` | Overwrite an existing file. Default `false`. |
| `--progress` | Show a progress bar. Default `true`. |

### `wvb builtin`

Download the currently deployed bundles from the remote into a local directory, to ship as
[builtin](/docs/concepts#sources-builtin-vs-remote) fallbacks with your app.

```sh
wvb builtin --endpoint https://updates.example.com --out .wvb/builtin/bundles
wvb builtin --include 'app*' --exclude 'internal*'
```

| Option | Description |
| ------------------------- | ------------------------------------------------- |
| `--out, -O` | Output directory. Default `.wvb/builtin/bundles`. |
| `--endpoint, -E` | Remote endpoint. |
| `--channel` | Channel to pull from. |
| `--include` / `--exclude` | Glob filters over remote bundles (repeatable). |
| `--clean` | Clear the output directory first. Default `true`. |
| `--concurrency` | Parallel downloads. |

## Inspecting & testing the remote

### `wvb remote list`

List bundles deployed on the remote.

```sh
wvb remote list --endpoint https://updates.example.com
wvb remote list --channel beta
```

### `wvb remote current [BUNDLE]`

Show the current deployed version and metadata for a bundle.

```sh
wvb remote current app --endpoint https://updates.example.com
```

### `wvb remote local`

Start a local update server backed by a directory (default `~/.wvb/local`). It implements the same
[HTTP contract](/docs/remote-updates#the-remote-http-contract) as a production server, so you can test
the full update loop offline.

```sh
wvb remote local # http://localhost:4313, serving ~/.wvb/local
wvb remote local --base-dir ./.wvb/local --port 4313 --allow-other-versions
```

| Option | Description |
| ------------------------ | -------------------------------------------------------- |
| `--base-dir` | Directory to serve. Default `~/.wvb/local`. |
| `--allow-other-versions` | Allow downloading non-current versions. Default `false`. |
| `--hostname, -H` | Bind hostname. Default `localhost`. (`HOSTNAME` env) |
| `--port, -P` | Port. Default `4313`. (`PORT` env) |
| `--silent` | Disable request logging. |

See [Remote updates → Testing locally](/docs/remote-updates#testing-locally) for how to wire this up
with `@wvb/remote-local`.
122 changes: 122 additions & 0 deletions content/docs/concepts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: Concepts
description: The .wvb format, bundle sources, the manifest, channels, integrity, and signatures.
---

This page defines the vocabulary used throughout the guides. If a term in a platform guide is
unclear, it's probably explained here.

## The `.wvb` archive

A Webview Bundle is a single file with three parts laid out back to back:

| Header (17 bytes) | Index (variable) | Data (variable) |
| -------------------------------------------------- | -------------------------------- | ---------------------------- |
| magic number, format version, index size, checksum | path → offset/length/headers map | LZ4-compressed file contents |

- **Header** — starts with the magic number `0xF09F8C90F09F8E81` (the UTF-8 bytes for 🌐🎁), then a
one-byte format version, a `u32` index size, and a checksum.
- **Index** — a map from each file path (e.g. `/index.html`) to where its bytes live in the data
section, plus its content type and any HTTP headers to replay when served.
- **Data** — each file's bytes compressed with [LZ4](https://github.com/lz4/lz4), each block
followed by an [xxHash-32](https://github.com/Cyan4973/xxHash) checksum.

Every section is checksummed, so a truncated or corrupted archive is detected before its contents
are trusted. The full byte-level specification is in the
[`wvb` crate docs](https://docs.rs/wvb).

By convention a packed file is named `<bundle_name>_<version>.wvb`, e.g. `app_1.0.0.wvb`.

## Bundles and bundle names

A **bundle** is one logical web app, identified by a **bundle name** (e.g. `app`). A bundle has
many **versions** (`1.0.0`, `1.1.0`, …), each a separate `.wvb` file. At any moment one version is
the **current** version that gets served.

## Sources: `builtin` vs `remote`

A **source** is a directory on the device that stores bundles, plus a `manifest.json` that tracks
versions. Apps typically use two sources:

- **`builtin`** — bundles shipped _inside_ the app package. Read-only, and used as the fallback the
very first time the app runs (before anything has been downloaded).
- **`remote`** — bundles downloaded from your update server. **Remote wins**: when a bundle exists
in both, the remote version is served.

```text
{source_dir}/
├── app/
│ ├── app_1.0.0.wvb
│ └── app_1.1.0.wvb
└── manifest.json
```

This split is what makes updates safe: you always have the shipped `builtin` version to fall back
to, and downloaded `remote` versions transparently shadow it once verified and installed.

## The manifest

`manifest.json` lives at the root of a source directory and records, for each bundle, the versions
present and which one is current:

```json
{
"manifestVersion": 1,
"entries": {
"app": {
"versions": {
"1.0.0": {},
"1.1.0": { "integrity": "sha384:…", "signature": "…", "etag": "…" }
},
"currentVersion": "1.1.0"
}
}
}
```

The per-version metadata (`integrity`, `signature`, `etag`, `lastModified`) is filled in from the
remote server's response headers when a bundle is downloaded.

## Channels

A **channel** lets you deploy different versions to different audiences from the same server —
`stable`, `beta`, `canary`, `internal`, and so on. Clients ask for a channel when listing,
downloading, or checking for updates; if they don't specify one, they get the default channel.
Channels are how you do staged rollouts and pre-release testing.

## Integrity

**Integrity** is a hash over the _serialized bytes_ of a bundle, so a client can confirm it
downloaded exactly what the server published. Webview Bundle uses SHA-3, serialized as
`<algorithm>:<base64>` — for example `sha384:Ws2q…`. Supported algorithms are `sha256`, `sha384`,
and `sha512`.

When updating, an **integrity policy** controls how strict verification is:

- **Strict** — a hash must be present and must match, or the update fails.
- **Optional** (default) — verify when a hash is present; allow the download when it isn't.
- **None** — skip integrity verification.

## Signatures

A **signature** proves _who_ published a bundle, not just that it is intact. The publisher signs
the bundle's integrity string with a private key; clients verify it with the matching public key.
Supported algorithms: **ECDSA** (secp256r1 / secp384r1), **Ed25519**, and **RSA** (PKCS#1 v1.5 /
PSS). Keys are supplied as PEM/DER. Signature verification is layered on top of integrity: if you
configure a verifier, downloads must carry a valid signature.

## The update lifecycle

Putting it together, shipping an update looks like this:

1. **Pack** your build output into a `.wvb` (`wvb pack`).
2. **Upload** it to your remote server, optionally computing integrity and a signature
(`wvb upload`).
3. **Deploy** that version to a channel so clients can see it (`wvb deploy`, or `wvb upload
--deploy`).
4. On the device, the **updater** checks for a newer deployed version, downloads it, verifies
integrity/signature, and installs it into the `remote` source — after which it's served instead
of the builtin version.

See [Remote updates & local testing](/docs/remote-updates) for the full walkthrough, including
how to run the whole loop on your own machine.
Loading