@openthink/ui-leaf 0.4.0 → 0.5.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 CHANGED
@@ -78,11 +78,11 @@ Three reasons:
78
78
 
79
79
  ## How it works
80
80
 
81
- `mount()` spins up a local dev server (rsbuild + React under the hood), bundles your view file, injects the data into `window.__UI_LEAF__.data`, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered. Browser tab close → heartbeat stops → server shuts down → `view.closed` resolves and your CLI continues.
81
+ `mount()` compiles your view file once with `Bun.build`, injects data into `window.__UI_LEAF__`, starts a `Bun.serve` HTTP server, and opens the user's default browser. Mutations from the view POST back to a localhost endpoint with a per-launch random token; the runtime dispatches them to the handlers you registered. Browser tab close → heartbeat stops → server shuts down → `view.closed` resolves and your CLI continues.
82
82
 
83
- The transport is HTTP + JSON over loopback. The token is in `window.__UI_LEAF__.token`, and it's served inline in the HTML at `/index.html` — so the token only protects against drive-by cross-origin requests in the user's browser, not against other processes on the same machine. Any local process that can reach `127.0.0.1:<port>` can fetch the page, grep the token out, and call `/mutate` with it; treat any local process you don't trust as having the same access as the view. View bundling resolves React from `ui-leaf`'s installed location, so your project doesn't need to install React.
83
+ The transport is HTTP + JSON over loopback. The token is in `window.__UI_LEAF__.token`, served inline in the compiled HTML — so the token only protects against drive-by cross-origin requests in the user's browser, not against other processes on the same machine. Any local process that can reach `127.0.0.1:<port>` can fetch `GET /`, grep the token out, and call `/mutate` or `/api/data` with it; treat any local process you don't trust as having the same access as the view. View bundling resolves React from `ui-leaf`'s installed location, so your project doesn't need to install React.
84
84
 
85
- The same trust boundary applies to the `data` you pass to `mount()`. The payload is JSON-inlined into `window.__UI_LEAF__.data` in the same `/index.html`, and is also written to `<tmpdir()>/ui-leaf-XXXXXX/index.html` on disk for the mount lifetime — readable by the same set of same-UID local processes that can read the token. For PHI, PCI, financial records, or anything else where a same-UID local reader is in your threat model, don't pass the sensitive payload through `data`; keep it in your CLI's memory and inject it into the view via an authenticated `connect-src 'self'` fetch on boot. See "Data-at-rest in the temp directory" below for the disk-residency details.
85
+ The same trust boundary applies to the `data` you pass to `mount()`. The payload is JSON-inlined into `window.__UI_LEAF__.data` in the served HTML and held in memory for the mount lifetime — readable by the same set of same-UID local processes that can read the token. For PHI, PCI, financial records, or anything else where a same-UID local reader is in your threat model, use `dataLoader` instead the loader's return value is served at a token-gated `/api/data` endpoint and never appears in the HTML. See "Data-at-rest" below for details.
86
86
 
87
87
  ## API surface
88
88
 
@@ -97,7 +97,7 @@ await mount({
97
97
  mutations, // Record<string, MutationHandler> (optional)
98
98
  viewsRoot, // optional, default: <cwd>/views
99
99
  title, // optional, default: "ui-leaf"
100
- port, // optional, default: 5810 (auto-bumps if busy)
100
+ port, // optional, default: 5810 (auto-bumps if busy; pass 0 for OS-assigned)
101
101
  openBrowser, // optional, default: true
102
102
  shell, // optional, "tab" | "app", default: "tab"
103
103
  csp, // optional, default: "off" (see Hardening)
@@ -130,7 +130,7 @@ mount({
130
130
 
131
131
  - **Locks `connect-src` to same-origin** — the architectural lock. Views cannot fetch external APIs; all data flows through `data` and `mutations`.
132
132
  - **Permits HTTPS images and fonts** so views can load CDN assets normally.
133
- - **Allows inline styles, eval, and inline scripts** for React + rsbuild's HMR.
133
+ - **Allows inline styles and inline scripts** for React.
134
134
 
135
135
  Because the policy is sent as an HTTP response header, views cannot relax it at runtime. The only way to weaken the policy is to change the `mount()` call (i.e. fork the consumer CLI, not the view).
136
136
 
@@ -156,22 +156,22 @@ mount({
156
156
 
157
157
  Be deliberate — every name you add becomes a viable rebinding target. Don't add public DNS names or LAN hostnames you don't fully control.
158
158
 
159
- ### Data-at-rest in the temp directory
159
+ ### Data-at-rest
160
160
 
161
- ui-leaf serialises the `data` you pass to `mount()` into `<tmpdir()>/ui-leaf-XXXXXX/index.html` so the dev server can serve it. The directory is created with `mode 0700` (readable only by the same UID), and ui-leaf removes it on `close()`, on `SIGINT`/`SIGTERM`, on uncaught throws via a `process.on('exit')` fallback, and opportunistically sweeps `ui-leaf-*` siblings older than 24h on every startup to catch anything that still slipped through.
161
+ The `data` you pass to `mount()` is JSON-inlined into the compiled HTML that `GET /` serves, and held in memory for the mount lifetime. It is **not written to disk** the compiled HTML exists only in process memory. A same-UID local process that can reach `127.0.0.1:<port>` can still recover the data by fetching `GET /` (no token required), so `data` is appropriate for routine payloads but not for PHI, PCI, or financial records where in-HTML exposure is in your threat model.
162
162
 
163
- What still leaks: `SIGKILL`, OOM-kill, and abrupt power loss skip every Node hook, so the directory stays on disk until the next `mount()` runs (the startup sweep) or the OS rotates `tmpdir()` (on macOS, only across reboots; on many Linux systems, only via `tmpfiles.d` age policies). If the data is sensitive enough that even that bounded window is too long, use `dataLoader` instead of `data`:
163
+ For sensitive payloads use `dataLoader` instead of `data`:
164
164
 
165
165
  ```ts
166
166
  mount({
167
167
  view: "report",
168
168
  dataLoader: async () => {
169
- return await db.fetchSensitiveRecords(); // stays in memory; never touches disk
169
+ return await db.fetchSensitiveRecords(); // stays in memory; never appears in HTML
170
170
  },
171
171
  });
172
172
  ```
173
173
 
174
- `dataLoader` invokes the function once at mount time, captures the result in-process, and serves it at a token-gated `GET /api/data` endpoint using the same per-launch token as `/mutate`. The view fetches `/api/data` on first render. Nothing is written to `index.html` or any tempdir file the payload stays in memory for the entire mount lifetime. `data` remains the ergonomic default for routine payloads where disk residency is not a concern.
174
+ `dataLoader` invokes the function once at mount time, captures the result in-process, and serves it at a token-gated `GET /api/data` endpoint using the same per-launch token as `/mutate`. The view fetches `/api/data` on first render. The payload never appears in the HTML it can only be read by a caller that already has the token. Note that a local process that can fetch `GET /` (no auth) can still read the token from `window.__UI_LEAF__.token` and then call `/api/data` so `dataLoader` hardens against HTML-level exposure, not against a determined same-UID reader.
175
175
 
176
176
  ## Sharing views across users
177
177
 
@@ -291,7 +291,7 @@ Each pending mutation has a unique `id`. Multiple mutations can be in flight con
291
291
 
292
292
  ### Driving from Node via `mount()` directly
293
293
 
294
- If your consumer is itself Node (or you want a thin in-process integration), use the SDK directly. Pass `silent: true` to suppress rsbuild output so you can keep stdout clean for your own protocol (capture `process.stdout.write` *before* calling `mount()`, since the option redirects stdout to stderr for the lifetime of the dev server):
294
+ If your consumer is itself Node (or you want a thin in-process integration), use the SDK directly. Pass `silent: true` to redirect stdout to stderr for the lifetime of the server, keeping stdout clean for your own protocol (capture `process.stdout.write` *before* calling `mount()` if you need to write to real stdout from the same process):
295
295
 
296
296
  ```ts
297
297
  const realStdoutWrite = process.stdout.write.bind(process.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openthink/ui-leaf",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Customizable browser views, on demand, for any CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Matt Pardini",
@@ -25,41 +25,38 @@
25
25
  },
26
26
  "exports": {
27
27
  ".": {
28
- "types": "./dist/index.d.ts",
29
- "import": "./dist/index.js"
28
+ "types": "./packages/cli/dist/index.d.ts",
29
+ "import": "./packages/cli/dist/index.js"
30
30
  },
31
31
  "./view": {
32
- "types": "./dist/view.d.ts",
33
- "import": "./dist/view.js"
32
+ "types": "./packages/cli/dist/view.d.ts",
33
+ "import": "./packages/cli/dist/view.js"
34
34
  }
35
35
  },
36
- "main": "./dist/index.js",
37
- "types": "./dist/index.d.ts",
36
+ "main": "./packages/cli/dist/index.js",
37
+ "types": "./packages/cli/dist/index.d.ts",
38
38
  "bin": {
39
- "ui-leaf": "./dist/cli.js"
39
+ "ui-leaf": "./packages/cli/dist/cli.js"
40
40
  },
41
41
  "files": [
42
- "dist",
42
+ "packages/cli/dist",
43
+ "packages/cli/package.json",
43
44
  "README.md",
44
45
  "LICENSE"
45
46
  ],
47
+ "workspaces": ["packages/*"],
46
48
  "scripts": {
47
- "build": "tsup",
48
- "dev": "tsup --watch",
49
- "typecheck": "tsc --noEmit",
49
+ "build": "bun run --filter '@openthink/ui-leaf-cli' build",
50
+ "dev": "bun run --filter '@openthink/ui-leaf-cli' dev",
51
+ "typecheck": "tsc --noEmit -p packages/cli/tsconfig.src.json",
50
52
  "test": "bun test",
51
53
  "prepublishOnly": "bun run build"
52
54
  },
53
55
  "devDependencies": {
54
56
  "@types/node": "^25.6.0",
55
- "@types/react": "^19.2.14",
56
- "@types/react-dom": "^19.2.3",
57
- "tsup": "^8.4.0",
58
57
  "typescript": "^6.0.0"
59
58
  },
60
59
  "dependencies": {
61
- "@rsbuild/core": "^2.0.1",
62
- "@rsbuild/plugin-react": "^2.0.0",
63
60
  "open": "^11.0.0",
64
61
  "react": "^19.2.5",
65
62
  "react-dom": "^19.2.5"