@pablozaiden/webapp 0.5.7 → 0.6.0
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/README.md +11 -3
- package/docs/auth-validation.md +11 -0
- package/docs/auth.md +25 -0
- package/docs/cli.md +73 -3
- package/docs/deployment.md +84 -7
- package/docs/getting-started.md +69 -6
- package/docs/github-actions.md +39 -0
- package/docs/release.md +5 -5
- package/docs/server.md +34 -4
- package/docs/settings.md +5 -0
- package/docs/sidebar.md +2 -0
- package/docs/ui-guidelines.md +12 -4
- package/package.json +2 -4
- package/src/build/build-binary.ts +67 -24
- package/src/cli/api-command.ts +53 -16
- package/src/cli/credentials.ts +275 -4
- package/src/cli/device-auth.ts +83 -22
- package/src/cli/environment-auth.ts +57 -0
- package/src/cli/index.ts +1 -0
- package/src/package-resolution.ts +34 -0
- package/src/server/auth/device-auth.ts +2 -2
- package/src/server/auth/passkeys.ts +16 -16
- package/src/server/auth/request-origin.ts +97 -16
- package/src/server/authentication.ts +265 -0
- package/src/server/create-web-app-server.ts +85 -1391
- package/src/server/framework-endpoints.ts +451 -0
- package/src/server/public-route-dispatch.ts +85 -0
- package/src/server/request-schemas.ts +144 -0
- package/src/server/responses.ts +69 -3
- package/src/server/route-dispatch.ts +109 -0
- package/src/server/runtime-config.ts +67 -0
- package/src/server/same-origin.ts +1 -1
- package/src/server/server-lifecycle.ts +138 -0
- package/src/server/server-types.ts +87 -0
- package/src/server/web-document.ts +532 -0
- package/src/web/WebAppRoot.tsx +69 -1132
- package/src/web/api-client.ts +14 -4
- package/src/web/app-shell.tsx +198 -0
- package/src/web/auth-screens.tsx +186 -0
- package/src/web/components/index.tsx +10 -3
- package/src/web/index.ts +1 -0
- package/src/web/mobile-hooks.ts +194 -0
- package/src/web/mobile.ts +12 -0
- package/src/web/root-types.ts +61 -0
- package/src/web/routing.ts +61 -0
- package/src/web/settings/account-section.tsx +52 -0
- package/src/web/settings/resource-state.tsx +17 -0
- package/src/web/settings/security-section.tsx +119 -0
- package/src/web/settings/sessions-section.tsx +66 -0
- package/src/web/settings/settings-view.tsx +96 -0
- package/src/web/settings/shutdown-section.tsx +95 -0
- package/src/web/settings/user-management.tsx +123 -0
- package/src/web/settings-view.tsx +3 -0
- package/src/web/sidebar-state.ts +108 -0
- package/src/web/sidebar-tree.tsx +89 -0
- package/src/web/styles.css +78 -61
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pablozaiden/webapp
|
|
2
2
|
|
|
3
|
-
Opinionated Bun + React framework for single-server TypeScript webapps: one Bun process serves the React UI, API routes, multi-user passkey auth, user-owned API keys, device auth, realtime websocket state, scoped settings, binary builds and Docker images.
|
|
3
|
+
Opinionated Bun + React framework for single-server TypeScript webapps: one Bun process serves the React UI, API routes, multi-user passkey auth, user-owned API keys, device auth, stateless CLI API-key auth, realtime websocket state, scoped settings, binary builds and Docker images.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -14,6 +14,10 @@ bun run dev:kitchen-sink
|
|
|
14
14
|
|
|
15
15
|
Both examples run with Bun native hot reload through `bun --hot`, with no standalone frontend build server and no external `WEB_DIST_DIR`.
|
|
16
16
|
|
|
17
|
+
## Application dependencies
|
|
18
|
+
|
|
19
|
+
Each application package must declare `react` and `react-dom`; they are peer dependencies of the framework. Bun resolves `react-dom/client` from the application package context, so workspace installs do not need to hoist React packages to a repository root. Native startup and binary builds fail early with the application context when `react-dom` is not installed.
|
|
20
|
+
|
|
17
21
|
## Main exports
|
|
18
22
|
|
|
19
23
|
| Export | Use |
|
|
@@ -21,7 +25,11 @@ Both examples run with Bun native hot reload through `bun --hot`, with no standa
|
|
|
21
25
|
| `@pablozaiden/webapp/server` | `createWebAppServer`, route helpers, responses, SQLite store |
|
|
22
26
|
| `@pablozaiden/webapp/web` | `WebAppRoot`, `renderWebApp`, sidebar types, UI controls, realtime hooks |
|
|
23
27
|
| `@pablozaiden/webapp/contracts` | Shared auth/config/device/API-key types |
|
|
24
|
-
| `@pablozaiden/webapp/cli` | One-binary command helpers, device
|
|
28
|
+
| `@pablozaiden/webapp/cli` | One-binary command helpers, device/environment auth credentials and generic API CLI caller |
|
|
25
29
|
| `@pablozaiden/webapp/build` | Bun single-binary compile helper |
|
|
26
30
|
|
|
27
|
-
See `docs/getting-started.md` for the minimum app shape and `examples/notes-todo` for a realistic app. Use `docs/github-actions.md` when adding CI, Docker and release workflows to an app built with the framework.
|
|
31
|
+
See `docs/getting-started.md` for the minimum app shape and `examples/notes-todo` for a realistic app. Use `docs/github-actions.md` when adding CI, Docker and release workflows to an app built with the framework. Release/publishing details for this package are in `docs/release.md`.
|
|
32
|
+
|
|
33
|
+
CLI API callers can use a stored device session or the stateless
|
|
34
|
+
`${PREFIX}_BASE_URL` and `${PREFIX}_API_KEY` environment pair; see
|
|
35
|
+
`docs/cli.md` for precedence and anonymous fallback behavior.
|
package/docs/auth-validation.md
CHANGED
|
@@ -40,6 +40,17 @@ Run this checklist before releasing a framework app or cutting a checkpoint.
|
|
|
40
40
|
4. Delete the key from Settings and confirm the confirmation dialog appears.
|
|
41
41
|
5. Confirm the deleted token no longer authenticates.
|
|
42
42
|
|
|
43
|
+
## CLI API-key authentication
|
|
44
|
+
|
|
45
|
+
1. Set `${PREFIX}_BASE_URL` and `${PREFIX}_API_KEY` for an app whose CLI passes
|
|
46
|
+
its `envPrefix` to `runApiCliCommand()`.
|
|
47
|
+
2. Run a protected API command and confirm it reaches the configured server
|
|
48
|
+
without starting an interactive device flow.
|
|
49
|
+
3. Remove either variable and confirm the request falls back to anonymous
|
|
50
|
+
behavior.
|
|
51
|
+
4. Use an expired, invalid, or insufficient-scope key and confirm the CLI
|
|
52
|
+
reports the server rejection without refreshing or retrying the key.
|
|
53
|
+
|
|
43
54
|
## Device auth
|
|
44
55
|
|
|
45
56
|
1. Call `POST /api/auth/device` with `client_id` and `scope`.
|
package/docs/auth.md
CHANGED
|
@@ -59,3 +59,28 @@ Device codes are one-use. Device sessions are self-only in Settings, and only ac
|
|
|
59
59
|
## Same-origin policy
|
|
60
60
|
|
|
61
61
|
Cookie/browser mutations require matching `Origin` or `Referer` by default. Use `sameOrigin: "never"` only for deliberate public/server-to-server routes; use `sameOrigin: "always"` for especially sensitive endpoints even when bearer tokens are used.
|
|
62
|
+
|
|
63
|
+
## Reverse-proxy request trust
|
|
64
|
+
|
|
65
|
+
Forwarded request headers are ignored by default. When
|
|
66
|
+
`{PREFIX}_TRUST_PROXY=true` is enabled, the policy explicitly selects the
|
|
67
|
+
supported `X-Forwarded-Proto`, `X-Forwarded-Host`, and
|
|
68
|
+
`X-Forwarded-Prefix` values through `{PREFIX}_TRUST_PROXY_HEADERS`. Comma
|
|
69
|
+
chains use the configured `{PREFIX}_TRUST_PROXY_CHAIN` value (`first` for the
|
|
70
|
+
left-most non-empty value or `last` for the right-most non-empty value).
|
|
71
|
+
Malformed or missing request values fall back to the direct request.
|
|
72
|
+
|
|
73
|
+
The same normalized request context drives expected same-origin origins,
|
|
74
|
+
WebSocket origin checks, passkey expected origins and RP IDs, the `Secure`
|
|
75
|
+
cookie attribute, cookie paths, setup links, and device/discovery URLs.
|
|
76
|
+
`{PREFIX}_PUBLIC_BASE_URL` is authoritative for origin, hostname, and secure
|
|
77
|
+
state even when forwarded headers are present; trusted prefixes remain
|
|
78
|
+
policy-controlled for path-bearing cookies and links.
|
|
79
|
+
|
|
80
|
+
Trust mode is a deployment boundary setting, not a way to trust arbitrary
|
|
81
|
+
client input. The reverse proxy must strip client-supplied forwarded headers
|
|
82
|
+
and overwrite only the values configured above, preferably as sanitized
|
|
83
|
+
single-value headers. Do not expose the application directly to untrusted
|
|
84
|
+
clients when trust mode is enabled. This release does not implement a
|
|
85
|
+
proxy-address allowlist, so the network boundary and proxy sanitization are
|
|
86
|
+
required.
|
package/docs/cli.md
CHANGED
|
@@ -25,7 +25,9 @@ const result = await dispatchCliCommand({
|
|
|
25
25
|
process.exitCode = printCliResult(result);
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Device-auth
|
|
28
|
+
Device-auth and environment API-key helpers can be composed into apps that
|
|
29
|
+
need authenticated CLI calls. Public-token commands, such as webhook
|
|
30
|
+
notification commands, can stay app-owned and do not need framework auth.
|
|
29
31
|
|
|
30
32
|
Route metadata can power generic API commands:
|
|
31
33
|
|
|
@@ -37,9 +39,77 @@ const catalog = createRouteCatalog(routes);
|
|
|
37
39
|
const result = await runApiCliCommand({
|
|
38
40
|
catalog,
|
|
39
41
|
args,
|
|
40
|
-
|
|
42
|
+
envPrefix: "MY_APP",
|
|
41
43
|
credentials,
|
|
44
|
+
// Optional explicit override; stored credentials provide the default URL.
|
|
45
|
+
// baseUrl: "https://app.example.test",
|
|
42
46
|
});
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
`runApiCliCommand` can list endpoints, print schema metadata, call endpoints
|
|
49
|
+
`runApiCliCommand` can list endpoints, print schema metadata, call endpoints
|
|
50
|
+
with `--method` and `--payload`, attach bearer credentials, and refresh stored
|
|
51
|
+
device credentials once on `401`.
|
|
52
|
+
When stored device credentials are available, their `baseUrl` is used by
|
|
53
|
+
default; pass `baseUrl` only to intentionally override that server. Without
|
|
54
|
+
stored credentials, the environment API-key rules below determine the request
|
|
55
|
+
URL.
|
|
56
|
+
|
|
57
|
+
## Stateless API-key authentication
|
|
58
|
+
|
|
59
|
+
An app that passes its validated `envPrefix` to `runApiCliCommand` can make
|
|
60
|
+
non-interactive authenticated requests with an API key:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export MY_APP_BASE_URL=https://app.example.test
|
|
64
|
+
export MY_APP_API_KEY='key-from-settings'
|
|
65
|
+
my-app api items
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The helper derives exactly `${PREFIX}_BASE_URL` and `${PREFIX}_API_KEY` from
|
|
69
|
+
the supplied prefix. When no stored device credentials are available, an
|
|
70
|
+
explicit `baseUrl` plus `${PREFIX}_API_KEY` takes precedence; otherwise the
|
|
71
|
+
complete environment pair supplies the request URL and bearer key. Values are
|
|
72
|
+
trimmed, and the base URL must use `http` or `https`; trailing slashes are
|
|
73
|
+
removed.
|
|
74
|
+
|
|
75
|
+
If either environment variable is missing or empty, the pair is ignored and
|
|
76
|
+
the request remains anonymous, using an explicit `baseUrl` or
|
|
77
|
+
`http://localhost:3000`. A partial pair is not an error. Stored device
|
|
78
|
+
credentials remain highest priority and continue to use their existing refresh
|
|
79
|
+
and `401` retry behavior.
|
|
80
|
+
|
|
81
|
+
The environment API-key path does not run interactive login or device
|
|
82
|
+
authorization, refresh tokens, write credential files, or retry rejected
|
|
83
|
+
requests. The key is supplied by the environment and kept in memory for the
|
|
84
|
+
request; it is not printed or persisted by the CLI. The server remains
|
|
85
|
+
responsible for API-key validity, expiry, and route scopes.
|
|
86
|
+
|
|
87
|
+
## Concurrent credential refreshes
|
|
88
|
+
|
|
89
|
+
`createJsonFileStore()` and `createDeviceCredentialsStore()` coordinate refreshes for
|
|
90
|
+
the same credential path with an adjacent lock file. When credentials are expired,
|
|
91
|
+
`refreshDeviceCredentials()` acquires that lock before calling the token endpoint,
|
|
92
|
+
reads the store again, and reuses credentials another process may have refreshed.
|
|
93
|
+
The write of newly refreshed credentials happens while the lock is held.
|
|
94
|
+
|
|
95
|
+
The `JsonFileStore<T>.withLock()` method is optional so existing custom stores that
|
|
96
|
+
only implement `path`, `read`, `write` and `clear` remain source-compatible. A
|
|
97
|
+
custom store must provide `read` and `withLock` to receive the same cross-process
|
|
98
|
+
refresh guarantee; otherwise the legacy refresh path is retained.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await store.withLock(async () => {
|
|
102
|
+
// Work serialized for this store path.
|
|
103
|
+
}, {
|
|
104
|
+
timeoutMs: 30_000,
|
|
105
|
+
staleAfterMs: 300_000,
|
|
106
|
+
pollIntervalMs: 25,
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The defaults are a 30-second acquisition timeout, a five-minute stale threshold
|
|
111
|
+
and a 25-millisecond polling interval. A timeout throws `JsonFileStoreLockError`
|
|
112
|
+
with `code === "timeout"`. Release failures use `code === "release"`. Lock
|
|
113
|
+
metadata contains only ownership information (PID, timestamp and nonce), never
|
|
114
|
+
access or refresh tokens. The credentials directory and file retain `0700` and
|
|
115
|
+
`0600` permissions respectively.
|
package/docs/deployment.md
CHANGED
|
@@ -17,6 +17,10 @@ await buildWebAppBinary({
|
|
|
17
17
|
});
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
The application package must declare both `react` and `react-dom` to satisfy the framework peer dependencies. The builder resolves `react-dom/client` through Bun from the application entrypoint, so strict or non-hoisted workspace layouts are supported. The browser dependency is bundled into the compiled binary; the build reports the application context and setup guidance before generating client output when `react-dom` cannot be resolved.
|
|
21
|
+
|
|
22
|
+
`getBunCompileTargetFromArgs` accepts at most one `--target=<value>` argument. Supported compile targets come from the `BUN_COMPILE_TARGETS` export from `@pablozaiden/webapp/build`; use that collection when selecting a target. Empty, malformed, duplicate, and unsupported values fail before the builder creates output directories or invokes Bun. Omit the option for the default local Bun binary.
|
|
23
|
+
|
|
20
24
|
The binary builder compiles the browser bundle with the framework defaults needed for `webapp` apps, including Tailwind CSS v4 processing. If an app needs extra browser build behavior, add Bun plugins or browser-only defines under `web.build`:
|
|
21
25
|
|
|
22
26
|
```ts
|
|
@@ -49,24 +53,97 @@ Keep server and CLI modes in the same binary. App-specific commands and framewor
|
|
|
49
53
|
|
|
50
54
|
## Docker
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
The example Dockerfiles copy a Bun-compiled Linux binary into a runtime image. They intentionally require `APP_BINARY`; there is no architecture-specific default. Build the binary for the container architecture first, then pass both the Docker platform and matching binary path:
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
# Linux amd64 (Bun calls this target x64)
|
|
60
|
+
bun run --cwd examples/notes-todo build --target=bun-linux-x64
|
|
61
|
+
docker buildx build \
|
|
62
|
+
--platform linux/amd64 \
|
|
63
|
+
--build-arg APP_BINARY=examples/notes-todo/dist/notes-todo-linux-x64 \
|
|
64
|
+
--file examples/notes-todo/Dockerfile \
|
|
65
|
+
--tag webapp-notes-todo:local \
|
|
66
|
+
--load \
|
|
67
|
+
.
|
|
68
|
+
|
|
69
|
+
# Linux arm64
|
|
58
70
|
bun run --cwd examples/kitchen-sink build --target=bun-linux-arm64
|
|
59
|
-
docker build
|
|
71
|
+
docker buildx build \
|
|
72
|
+
--platform linux/arm64 \
|
|
73
|
+
--build-arg APP_BINARY=examples/kitchen-sink/dist/kitchen-sink-linux-arm64 \
|
|
74
|
+
--file examples/kitchen-sink/Dockerfile \
|
|
75
|
+
--tag webapp-kitchen-sink:local \
|
|
76
|
+
--load \
|
|
77
|
+
.
|
|
60
78
|
```
|
|
61
79
|
|
|
62
|
-
|
|
80
|
+
Use the same mapping for either example: Docker `linux/amd64` requires the Bun `bun-linux-x64` build and `*-linux-x64` artifact, while Docker `linux/arm64` requires `bun-linux-arm64` and `*-linux-arm64`. To build an x64 image from an ARM64 host, select `--platform linux/amd64` and the x64 artifact explicitly; Docker Desktop or Buildx may need emulation enabled. Omitting `APP_BINARY`, selecting an unsupported target, or pairing the wrong artifact with the target fails during the image build.
|
|
81
|
+
|
|
82
|
+
The example Dockerfiles default to `node:22-bookworm` as a readily available Linux runtime base because the app itself is already a self-contained Bun binary. The container writes both framework and example application data to `/app/data`; mount a Docker volume there when data must persist beyond the container.
|
|
83
|
+
|
|
84
|
+
### Example application data files
|
|
85
|
+
|
|
86
|
+
Each example uses the configured data directory for two separate persistence
|
|
87
|
+
boundaries:
|
|
88
|
+
|
|
89
|
+
| Application | Framework database | Application database |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| Notes TODO | `webapp.sqlite` | `notes-todo.sqlite` |
|
|
92
|
+
| Kitchen Sink | `webapp.sqlite` | `kitchen-sink.sqlite` |
|
|
93
|
+
|
|
94
|
+
`webapp.sqlite` contains framework authentication and settings state. The
|
|
95
|
+
example database contains only that example's sections, notes, todos, or
|
|
96
|
+
projects; application-specific tables are not added to the framework store.
|
|
97
|
+
The example starts by creating or migrating its schema, and owner seed data is
|
|
98
|
+
transactional and idempotent across restarts.
|
|
99
|
+
|
|
100
|
+
Persist the complete `/app/data` directory, not just `webapp.sqlite`, when
|
|
101
|
+
deploying an example. A container filesystem, process restart, replacement
|
|
102
|
+
deployment, or reverse proxy does not provide durability by itself. Retain
|
|
103
|
+
and back up the volume mounted at `/app/data`; changing the data directory
|
|
104
|
+
intentionally starts a new application state.
|
|
105
|
+
|
|
106
|
+
The container should set:
|
|
63
107
|
|
|
64
108
|
```dockerfile
|
|
65
109
|
ENV MY_APP_HOST=0.0.0.0
|
|
66
110
|
ENV MY_APP_PORT=3000
|
|
67
|
-
ENV MY_APP_DATA_DIR=/data
|
|
111
|
+
ENV MY_APP_DATA_DIR=/app/data
|
|
68
112
|
```
|
|
69
113
|
|
|
70
114
|
The framework has no external frontend asset directory, so the compiled binary contains the server and Bun HTML import graph.
|
|
71
115
|
|
|
116
|
+
## Reverse proxy and forwarded headers
|
|
117
|
+
|
|
118
|
+
When TLS or a public path is terminated by a reverse proxy, keep the
|
|
119
|
+
application port reachable only from that proxy and configure the trust policy
|
|
120
|
+
explicitly:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
MY_APP_TRUST_PROXY=true
|
|
124
|
+
MY_APP_TRUST_PROXY_HEADERS=proto,host,prefix
|
|
125
|
+
MY_APP_TRUST_PROXY_CHAIN=first
|
|
126
|
+
MY_APP_PUBLIC_BASE_URL=https://app.example.test
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The proxy must strip any client-provided `X-Forwarded-Proto`,
|
|
130
|
+
`X-Forwarded-Host`, and `X-Forwarded-Prefix` values before overwriting the
|
|
131
|
+
headers it is responsible for. Only enable the corresponding symbolic
|
|
132
|
+
headers in `MY_APP_TRUST_PROXY_HEADERS`. Sanitized single-value headers are
|
|
133
|
+
recommended; `MY_APP_TRUST_PROXY_CHAIN` exists only to make a comma-separated
|
|
134
|
+
chain deterministic. The framework does not infer trust from header presence,
|
|
135
|
+
does not parse the RFC `Forwarded` header, and does not provide a
|
|
136
|
+
proxy-address allowlist in this release.
|
|
137
|
+
|
|
138
|
+
Use `MY_APP_PUBLIC_BASE_URL` when the external origin must remain deterministic
|
|
139
|
+
regardless of request headers. A trusted prefix controls cookie paths and
|
|
140
|
+
framework-generated setup/device links. If trust is disabled, the framework
|
|
141
|
+
uses the direct request URL and host, which is the correct default for a
|
|
142
|
+
direct deployment.
|
|
143
|
+
|
|
144
|
+
Proxy configuration does not make application data persistent. Set
|
|
145
|
+
`MY_APP_DATA_DIR` to a durable directory and mount a persistent volume for
|
|
146
|
+
container deployments; the SQLite data must survive independently of the
|
|
147
|
+
reverse proxy or application process.
|
|
148
|
+
|
|
72
149
|
For full application CI/CD templates, including a production Dockerfile, PR checks, GHCR publishing on `main`, binary releases and Docker release images, see `docs/github-actions.md`.
|
package/docs/getting-started.md
CHANGED
|
@@ -3,20 +3,23 @@
|
|
|
3
3
|
Apps are one Bun application: backend routes, websocket, React UI and static assets are served by the same server. The framework does not support or require a standalone client dist directory.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import { createWebAppServer, defineRoutes, jsonResponse, parseJson } from "@pablozaiden/webapp/server";
|
|
7
8
|
|
|
8
9
|
const items: Array<{ id: string; userId: string; title: string }> = [];
|
|
10
|
+
const itemCreateSchema = z.object({ title: z.string() });
|
|
9
11
|
|
|
10
12
|
const routes = defineRoutes({
|
|
11
13
|
"/api/items": {
|
|
12
14
|
auth: "user",
|
|
15
|
+
requestSchema: itemCreateSchema,
|
|
13
16
|
description: "List or create items.",
|
|
14
17
|
cliPath: "items",
|
|
15
18
|
tags: ["items"],
|
|
16
19
|
GET: (_req, ctx) => jsonResponse(ctx.filterOwned(items)),
|
|
17
20
|
async POST(req, ctx) {
|
|
18
21
|
const user = ctx.requireUser();
|
|
19
|
-
const body = await parseJson
|
|
22
|
+
const body = await parseJson(req, itemCreateSchema);
|
|
20
23
|
const item = { id: crypto.randomUUID(), userId: user.id, title: body.title };
|
|
21
24
|
items.push(item);
|
|
22
25
|
ctx.userRealtime.publishEntityChanged("items", item.id);
|
|
@@ -80,6 +83,18 @@ my-app api items
|
|
|
80
83
|
my-app notify --message "optional app-owned command"
|
|
81
84
|
```
|
|
82
85
|
|
|
86
|
+
For a non-interactive authenticated API command, pass the app's `envPrefix`
|
|
87
|
+
to `runApiCliCommand()` and provide the exact environment pair:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export MY_APP_BASE_URL=https://app.example.test
|
|
91
|
+
export MY_APP_API_KEY='key-from-settings'
|
|
92
|
+
my-app api items
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The pair is used when no stored device credentials are available. A missing
|
|
96
|
+
or partial pair preserves anonymous CLI requests.
|
|
97
|
+
|
|
83
98
|
See `docs/cli.md` for framework CLI helpers and generic API command support.
|
|
84
99
|
|
|
85
100
|
Frontend entrypoints should use `renderWebApp` so Bun/browser hot reload reuses the existing React root instead of calling `createRoot()` twice. Import the framework CSS explicitly so Bun hot reload observes style changes:
|
|
@@ -107,7 +122,19 @@ renderWebApp(
|
|
|
107
122
|
```
|
|
108
123
|
|
|
109
124
|
`renderWebApp` renders into `#root` by default and reuses the existing React root across hot reloads. Pass a custom element id or `Element` only when the app uses a different mount point.
|
|
110
|
-
`WebAppRoot` owns the shell and `.wapp-main-content`; each route component should return a `Page` wrapper so standard content margins, mobile padding and scroll behavior stay consistent.
|
|
125
|
+
`WebAppRoot` owns the shell and `.wapp-main-content`; each route component should return a `Page` wrapper so standard content margins, mobile padding and scroll behavior stay consistent. `Page` uses the padded layout by default. For routes whose child content fills the available shell viewport and owns its own spacing or scrolling, use the framework full layout instead of overriding framework CSS:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
function TerminalRoute() {
|
|
129
|
+
return (
|
|
130
|
+
<Page layout="full">
|
|
131
|
+
<Terminal />
|
|
132
|
+
</Page>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The `full` layout removes the page gutters and provides a flex-sized, overflow-contained surface for viewport-sized content. Use it for terminals, editors, previews and similar surfaces; the child should provide any internal padding and scrolling it needs.
|
|
111
138
|
|
|
112
139
|
Use the framework URL helpers for browser API calls, websocket URLs and app-local links instead of deriving paths from `window.location` in each app. They honor `<base>`, explicit `publicBasePath` config, and reverse-proxy subpaths for direct path deep links such as `/workspaces`:
|
|
113
140
|
|
|
@@ -115,6 +142,7 @@ Use the framework URL helpers for browser API calls, websocket URLs and app-loca
|
|
|
115
142
|
import {
|
|
116
143
|
appAbsoluteUrl,
|
|
117
144
|
appFetch,
|
|
145
|
+
appJson,
|
|
118
146
|
appPath,
|
|
119
147
|
appRequest,
|
|
120
148
|
appWebSocketUrl,
|
|
@@ -124,7 +152,7 @@ import {
|
|
|
124
152
|
|
|
125
153
|
configureWebAppClient();
|
|
126
154
|
|
|
127
|
-
const config = await
|
|
155
|
+
const config = await appJson("/api/config");
|
|
128
156
|
setWebAppPublicBasePath(config.publicBasePath);
|
|
129
157
|
|
|
130
158
|
const downloadUrl = appPath("/api/items/export");
|
|
@@ -133,7 +161,7 @@ const socket = new WebSocket(appWebSocketUrl("/api/ws"));
|
|
|
133
161
|
const rawResponse = await appRequest("/api/items");
|
|
134
162
|
```
|
|
135
163
|
|
|
136
|
-
`
|
|
164
|
+
`appJson` is the framework helper for successful JSON responses and builds on `appFetch`. Both `appJson` and `appFetch` honor configured URLs, credentials, auth-required events, and `WebAppApiError` responses. Use `appFetch` when a successful response body does not need to be parsed, and use `appRequest` when the app needs a raw response without the framework's error normalization, such as downloads or custom error handling. `appJson` intentionally rejects successful empty or non-JSON responses instead of fabricating a default value.
|
|
137
165
|
|
|
138
166
|
Recommended dev script:
|
|
139
167
|
|
|
@@ -151,11 +179,46 @@ The app configures an uppercase `envPrefix`; the framework reads only variables
|
|
|
151
179
|
| --- | --- | --- |
|
|
152
180
|
| `{PREFIX}_HOST` | `localhost` | Bind host |
|
|
153
181
|
| `{PREFIX}_PORT` | `3000` | Bind port |
|
|
154
|
-
| `{PREFIX}_DATA_DIR` | `./data` | SQLite persistence directory |
|
|
182
|
+
| `{PREFIX}_DATA_DIR` | `./data` | Durable SQLite persistence directory for framework auth and app data |
|
|
155
183
|
| `{PREFIX}_LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error`; locks settings log-level control when set |
|
|
156
184
|
| `{PREFIX}_DISABLE_PASSKEY` | unset | Emergency bypass that logs in as the existing owner; it does not create users |
|
|
157
185
|
| `{PREFIX}_DISABLE_SAME_ORIGIN_CHECK` | unset | Development/testing escape hatch |
|
|
158
|
-
| `{PREFIX}
|
|
186
|
+
| `{PREFIX}_TRUST_PROXY` | `false` | Explicitly trust the documented `X-Forwarded-*` headers; keep disabled for direct deployments |
|
|
187
|
+
| `{PREFIX}_TRUST_PROXY_HEADERS` | `proto,host,prefix` when enabled | Comma-separated subset of supported forwarded values: `proto`, `host`, `prefix` |
|
|
188
|
+
| `{PREFIX}_TRUST_PROXY_CHAIN` | `first` | Select the left-most (`first`) or right-most (`last`) non-empty value from comma-separated forwarded headers |
|
|
189
|
+
| `{PREFIX}_PUBLIC_BASE_URL` | request origin | Authoritative external origin for auth/device URLs; it overrides forwarded protocol and host values |
|
|
159
190
|
| `{PREFIX}_AUTH_ISSUER` | `urn:{prefix}:webapp` | JWT issuer override |
|
|
160
191
|
|
|
192
|
+
The configured data directory contains the framework-owned
|
|
193
|
+
`webapp.sqlite`. The example applications keep their application entities in
|
|
194
|
+
separate files in that same directory: Notes TODO uses `notes-todo.sqlite`
|
|
195
|
+
for sections, notes and todos, while Kitchen Sink uses `kitchen-sink.sqlite`
|
|
196
|
+
for projects. Keeping these files separate prevents example-specific tables
|
|
197
|
+
from being added to the framework authentication store.
|
|
198
|
+
|
|
199
|
+
Example application databases create or migrate their schema during startup.
|
|
200
|
+
Owner seed data is written transactionally and is idempotent across repeated
|
|
201
|
+
starts. Point the prefixed `*_DATA_DIR` at storage that survives process
|
|
202
|
+
restarts and redeployments; deleting or changing the directory intentionally
|
|
203
|
+
starts a new application state.
|
|
204
|
+
|
|
205
|
+
For a reverse-proxy deployment that strips and overwrites the forwarded
|
|
206
|
+
headers, configure the trust policy explicitly:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
MY_APP_TRUST_PROXY=true
|
|
210
|
+
MY_APP_TRUST_PROXY_HEADERS=proto,host,prefix
|
|
211
|
+
MY_APP_TRUST_PROXY_CHAIN=first
|
|
212
|
+
MY_APP_PUBLIC_BASE_URL=https://app.example.test
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The default is fail-closed: forwarded headers are ignored unless
|
|
216
|
+
`TRUST_PROXY=true`. The framework recognizes only `X-Forwarded-Proto`,
|
|
217
|
+
`X-Forwarded-Host`, and `X-Forwarded-Prefix`; it does not infer trust from
|
|
218
|
+
their presence and does not parse the RFC `Forwarded` header. A trusted prefix
|
|
219
|
+
is used for cookie paths and externally generated links. The proxy must remove
|
|
220
|
+
client-supplied values before writing these headers, and direct untrusted
|
|
221
|
+
access to the application port must be blocked. This configuration does not
|
|
222
|
+
provide a proxy-address allowlist.
|
|
223
|
+
|
|
161
224
|
For CI/CD setup in apps built with the framework, use `docs/github-actions.md`. It includes copy-paste GitHub Actions and a production Dockerfile template.
|
package/docs/github-actions.md
CHANGED
|
@@ -75,6 +75,39 @@ CMD ["/app/my-app", "serve"]
|
|
|
75
75
|
|
|
76
76
|
For apps that need extra OS packages, add them to the runtime `apt-get install` list. Keep `ca-certificates`, `curl` and `tini`.
|
|
77
77
|
|
|
78
|
+
### Prebuilt-binary Dockerfiles
|
|
79
|
+
|
|
80
|
+
The Dockerfile above builds the binary inside Docker, so it does not use `APP_BINARY`. The repository examples use a different pattern: they copy a prebuilt binary and require the caller to provide the artifact explicitly. Keep the platform and artifact mapping below in local commands and GitHub Actions:
|
|
81
|
+
|
|
82
|
+
| Docker platform | Bun build target | Required artifact suffix |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `linux/amd64` | `bun-linux-x64` | `linux-x64` |
|
|
85
|
+
| `linux/arm64` | `bun-linux-arm64` | `linux-arm64` |
|
|
86
|
+
|
|
87
|
+
For a prebuilt-binary Dockerfile, do not add an architecture-specific `APP_BINARY` default. Build and image commands should select the same platform and artifact, for example:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bun run --cwd examples/notes-todo build --target=bun-linux-x64
|
|
91
|
+
docker buildx build \
|
|
92
|
+
--platform linux/amd64 \
|
|
93
|
+
--build-arg APP_BINARY=examples/notes-todo/dist/notes-todo-linux-x64 \
|
|
94
|
+
--file examples/notes-todo/Dockerfile \
|
|
95
|
+
--tag webapp-notes-todo:amd64 \
|
|
96
|
+
--load \
|
|
97
|
+
.
|
|
98
|
+
|
|
99
|
+
bun run --cwd examples/notes-todo build --target=bun-linux-arm64
|
|
100
|
+
docker buildx build \
|
|
101
|
+
--platform linux/arm64 \
|
|
102
|
+
--build-arg APP_BINARY=examples/notes-todo/dist/notes-todo-linux-arm64 \
|
|
103
|
+
--file examples/notes-todo/Dockerfile \
|
|
104
|
+
--tag webapp-notes-todo:arm64 \
|
|
105
|
+
--load \
|
|
106
|
+
.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Validate `TARGETOS` and `TARGETARCH` in the Dockerfile before copying the artifact, so omitted, unsupported, or mismatched inputs fail during the build rather than at container startup. When building ARM64 images on an x64 GitHub runner, configure QEMU and Buildx before the build.
|
|
110
|
+
|
|
78
111
|
## Pull request workflow
|
|
79
112
|
|
|
80
113
|
Place this at `.github/workflows/pr.yml`.
|
|
@@ -209,6 +242,8 @@ Place this at `.github/workflows/docker-main.yml`.
|
|
|
209
242
|
|
|
210
243
|
It publishes `ghcr.io/<owner>/<repo>:main` after merges to `main`, updates `package.json` to a pre-release version based on the latest GitHub release, and runs a container health smoke test.
|
|
211
244
|
|
|
245
|
+
Keep the `platforms` value explicit. The sample uses `linux/amd64` because `load: true` loads one runnable image for the smoke test; do not omit the value and rely on the runner architecture. To publish a multi-platform `main` image, configure QEMU, use `platforms: linux/amd64,linux/arm64`, and smoke-test platform-specific loaded images before pushing the multi-platform manifest, because Docker cannot load a multi-platform result into the local image store as one runnable tag.
|
|
246
|
+
|
|
212
247
|
```yaml
|
|
213
248
|
name: Docker Main
|
|
214
249
|
|
|
@@ -332,6 +367,8 @@ Place this at `.github/workflows/docker-release.yml`.
|
|
|
332
367
|
|
|
333
368
|
It publishes versioned GHCR images when a GitHub release is published:
|
|
334
369
|
|
|
370
|
+
This release workflow is intentionally multi-platform. Keep both QEMU and Buildx setup steps and keep `platforms: linux/amd64,linux/arm64` explicit; the target platform, not the runner's architecture, determines the image binary.
|
|
371
|
+
|
|
335
372
|
```yaml
|
|
336
373
|
name: Docker Release
|
|
337
374
|
|
|
@@ -415,6 +452,8 @@ await buildWebAppBinary({
|
|
|
415
452
|
});
|
|
416
453
|
```
|
|
417
454
|
|
|
455
|
+
`getBunCompileTargetFromArgs` accepts one `--target=<value>` option and validates it against the framework's exported `BUN_COMPILE_TARGETS` list before the build starts. Omitted targets build for the local Bun runtime; malformed, duplicate, empty, or unsupported target options fail immediately.
|
|
456
|
+
|
|
418
457
|
`buildWebAppBinary` includes the framework browser build defaults, including Tailwind CSS v4 processing. Apps that need additional browser-only transforms can pass Bun plugins or defines through `web.build.plugins` and `web.build.define`; app plugins run before the framework defaults.
|
|
419
458
|
|
|
420
459
|
## Adaptation checklist
|
package/docs/release.md
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
`Main Docker Smoke` runs after merges to `main`:
|
|
16
16
|
|
|
17
|
-
1. Build Linux x64 binaries for both examples.
|
|
18
|
-
2. Build Docker
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
17
|
+
1. Build Linux x64 and Linux arm64 binaries for both examples.
|
|
18
|
+
2. Build each Docker image with an explicit `linux/amd64` or `linux/arm64` platform and matching `APP_BINARY` path.
|
|
19
|
+
3. Validate missing, unsupported, and mismatched Docker arguments fail during the image build.
|
|
20
|
+
4. Run both architecture variants of both containers.
|
|
21
|
+
5. Smoke-test `/api/health` plus one app endpoint through the published container ports.
|
|
21
22
|
|
|
22
23
|
## NPM releases
|
|
23
24
|
|
|
@@ -61,4 +62,3 @@ For the first publish, npm may require a local manual publish before trusted pub
|
|
|
61
62
|
- Workflow: `release-npm-package.yml`
|
|
62
63
|
- Environment: leave empty unless an environment is added later
|
|
63
64
|
7. After that, publish future versions by creating and publishing GitHub releases with tags like `v0.1.1`.
|
|
64
|
-
|
package/docs/server.md
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
Use `createWebAppServer` with `defineRoutes`. Route patterns support exact path segments and `:params`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const projectCreateSchema = z.object({ name: z.string() });
|
|
9
|
+
const projectUpdateSchema = z.object({ name: z.string().optional() });
|
|
10
|
+
|
|
6
11
|
const routes = defineRoutes<AppEvent>({
|
|
7
12
|
"/api/projects": {
|
|
8
13
|
auth: "user",
|
|
14
|
+
requestSchema: projectCreateSchema,
|
|
9
15
|
description: "List or create projects.",
|
|
10
16
|
cliPath: "projects",
|
|
11
17
|
tags: ["projects"],
|
|
@@ -14,7 +20,7 @@ const routes = defineRoutes<AppEvent>({
|
|
|
14
20
|
},
|
|
15
21
|
async POST(req, ctx) {
|
|
16
22
|
const user = ctx.requireUser();
|
|
17
|
-
const body = await parseJson
|
|
23
|
+
const body = await parseJson(req, projectCreateSchema);
|
|
18
24
|
const project = createProject(user.id, body.name);
|
|
19
25
|
ctx.userRealtime.publishEntityChanged("projects", project.id);
|
|
20
26
|
return jsonResponse(project);
|
|
@@ -23,9 +29,12 @@ const routes = defineRoutes<AppEvent>({
|
|
|
23
29
|
"/api/projects/:id": {
|
|
24
30
|
auth: "user",
|
|
25
31
|
scopes: ["projects:write"],
|
|
32
|
+
requestSchema: projectUpdateSchema,
|
|
26
33
|
async PATCH(req, ctx) {
|
|
27
34
|
const project = ctx.requireOwned(await findProject(ctx.params.id));
|
|
28
|
-
|
|
35
|
+
const body = await parseJson(req, projectUpdateSchema);
|
|
36
|
+
if (body.name !== undefined) project.name = body.name;
|
|
37
|
+
project.updatedAt = new Date().toISOString();
|
|
29
38
|
ctx.userRealtime.publishEntityChanged("projects", project.id);
|
|
30
39
|
return jsonResponse(project);
|
|
31
40
|
},
|
|
@@ -37,6 +46,27 @@ const routes = defineRoutes<AppEvent>({
|
|
|
37
46
|
});
|
|
38
47
|
```
|
|
39
48
|
|
|
49
|
+
Request schemas define the writable input fields, but handlers should still assign
|
|
50
|
+
those fields explicitly rather than spreading or merging parsed input into a
|
|
51
|
+
stored record. The example schemas use Zod's default object behavior, so
|
|
52
|
+
unknown properties are ignored and never persisted; keep `id`, `userId`,
|
|
53
|
+
timestamps, and other server-managed fields under application control.
|
|
54
|
+
|
|
55
|
+
`parseJson(req, schema)` parses and validates the body at runtime. Malformed JSON
|
|
56
|
+
returns a 400 `invalid_json` response, while a JSON value that does not satisfy
|
|
57
|
+
the schema returns a 400 `invalid_request_body` response with field details.
|
|
58
|
+
Use `parseOptionalJson(req, schema)` only for endpoints that deliberately allow
|
|
59
|
+
an empty body. Only a zero-byte body is considered absent; whitespace-only
|
|
60
|
+
content is non-empty malformed JSON and is rejected just like any other
|
|
61
|
+
malformed body. `parseUnknownJson` returns `unknown` and is intentionally
|
|
62
|
+
unvalidated, so application handlers should prefer a schema-backed parser.
|
|
63
|
+
|
|
64
|
+
The Notes TODO webhook uses an absent body (or an object without `title`) to
|
|
65
|
+
apply its source-based fallback title. When an owner does not exist, that
|
|
66
|
+
accepted delivery returns 202 with `accepted: false`; malformed or
|
|
67
|
+
schema-invalid bodies are rejected before this ownerless response or any
|
|
68
|
+
mutation is reached.
|
|
69
|
+
|
|
40
70
|
Route defaults are intentionally secure:
|
|
41
71
|
|
|
42
72
|
| Setting | Default | Meaning |
|
|
@@ -55,7 +85,7 @@ Route definitions can include optional metadata. This keeps the API route table
|
|
|
55
85
|
| `description` | Human-readable route description |
|
|
56
86
|
| `cliPath` | CLI-friendly path; defaults to the API path without `/api/` |
|
|
57
87
|
| `tags` | Grouping labels for docs/CLI |
|
|
58
|
-
| `requestSchema`, `querySchema`, `responseSchema` | Optional schema objects for CLI/docs |
|
|
88
|
+
| `requestSchema`, `querySchema`, `responseSchema` | Optional schema objects for CLI/docs; use the same runtime `requestSchema` with `parseJson` when a route accepts a body |
|
|
59
89
|
| `catalog: false` | Exclude a route from generated catalogs |
|
|
60
90
|
|
|
61
91
|
Use `createRouteCatalog(routes)` and `findRouteCatalogEntry(catalog, input)` to power app CLI commands without maintaining a second route catalog.
|
|
@@ -96,7 +126,7 @@ Built-in endpoints include:
|
|
|
96
126
|
|
|
97
127
|
## Framework-owned web document and PWA metadata
|
|
98
128
|
|
|
99
|
-
`createWebAppServer` owns the browser document. Apps do not provide `index.html`; the framework generates a Bun `HTMLBundle` internally so Bun hot reload and asset rewriting keep working. By default the frontend entrypoint is `./web/main.tsx` relative to the Bun entry file. The generated viewport keeps the app at `initial-scale=1` with `maximum-scale=1` and `user-scalable=no`; clients and mobile browsers that honor those viewport scaling tokens, including iPhone and iPad, cannot change the app scale with pinch-to-zoom while normal scrolling remains enabled. Clients that ignore the tokens are unaffected.
|
|
129
|
+
`createWebAppServer` owns the browser document. Apps do not provide `index.html`; the framework generates a Bun `HTMLBundle` internally so Bun hot reload and asset rewriting keep working. By default the frontend entrypoint is `./web/main.tsx` relative to the Bun entry file. The generated document also initializes the shared `data-wapp-mobile` state before client styles load so CSS and `WebAppRoot` use the same mobile breakpoint. The generated viewport keeps the app at `initial-scale=1` with `maximum-scale=1` and `user-scalable=no`; clients and mobile browsers that honor those viewport scaling tokens, including iPhone and iPad, cannot change the app scale with pinch-to-zoom while normal scrolling remains enabled. Clients that ignore the tokens are unaffected.
|
|
100
130
|
|
|
101
131
|
Do not replace this behavior with global touch event handlers, `preventDefault()` calls, or `touch-action: none`: those approaches can disable scrolling and other touch interactions. Applications should keep the document framework-owned rather than adding an app-owned `index.html`.
|
|
102
132
|
|
package/docs/settings.md
CHANGED
|
@@ -11,6 +11,11 @@ Settings is framework-owned so apps stay consistent. It includes:
|
|
|
11
11
|
- Admin server kill
|
|
12
12
|
- Version/about
|
|
13
13
|
|
|
14
|
+
The local-storage or system theme is applied immediately while the signed-in
|
|
15
|
+
user preference loads in the background. If that request fails, the current
|
|
16
|
+
theme remains active and Display Settings shows a non-blocking error with a
|
|
17
|
+
retry action; the rest of Settings remains usable.
|
|
18
|
+
|
|
14
19
|
Security lists only show useful active credentials. Expired API keys are purged before listing, and revoked or expired device-auth refresh sessions are not shown. Revoked refresh sessions may remain in storage when needed for token-reuse protection, but they are hidden from Settings.
|
|
15
20
|
|
|
16
21
|
Destructive actions in Settings use the framework `ConfirmDialog` before mutating. This includes deleting users, deleting API keys, deleting passkeys, revoking device-auth sessions, and killing the server.
|
package/docs/sidebar.md
CHANGED
|
@@ -38,6 +38,8 @@ Sidebar nodes support:
|
|
|
38
38
|
|
|
39
39
|
Search is intentionally app-defined: `getNodes({ search })` receives raw search text and returns the tree that should be rendered. Set `sidebar.search: false` when an app has a small fixed navigation tree and should not show the sidebar search box.
|
|
40
40
|
|
|
41
|
+
On mobile widths, the drawer can be opened with a horizontal swipe starting within the first 24px of the viewport, in addition to the header button. The gesture must move at least 64px to the right, stay within 48px of vertical displacement, and remain more horizontal than vertical.
|
|
42
|
+
|
|
41
43
|
Use `actions` when an entity needs commands in the sidebar. `WebAppRoot` finds the active route-backed sidebar node and automatically renders its `ActionMenuItem[]` in the title-bar three-line menu, so the sidebar right-click menu and header menu stay consistent from one source of truth. Use `header.getActions` only for extra route-level actions that are not represented by the active sidebar node.
|
|
42
44
|
|
|
43
45
|
```tsx
|