@openthink/ui-leaf 0.3.3 → 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
 
@@ -92,11 +92,12 @@ import type { ViewProps, MutationHandler } from "@openthink/ui-leaf/view";
92
92
 
93
93
  await mount({
94
94
  view, // resolves <viewsRoot>/<view>.tsx
95
- data, // JSON-serializable, becomes data prop
95
+ data, // JSON-serializable, becomes data prop (convenience default)
96
+ dataLoader, // optional async fn; serves data via authenticated /api/data (no disk write)
96
97
  mutations, // Record<string, MutationHandler> (optional)
97
98
  viewsRoot, // optional, default: <cwd>/views
98
99
  title, // optional, default: "ui-leaf"
99
- port, // optional, default: 5810 (auto-bumps if busy)
100
+ port, // optional, default: 5810 (auto-bumps if busy; pass 0 for OS-assigned)
100
101
  openBrowser, // optional, default: true
101
102
  shell, // optional, "tab" | "app", default: "tab"
102
103
  csp, // optional, default: "off" (see Hardening)
@@ -129,7 +130,7 @@ mount({
129
130
 
130
131
  - **Locks `connect-src` to same-origin** — the architectural lock. Views cannot fetch external APIs; all data flows through `data` and `mutations`.
131
132
  - **Permits HTTPS images and fonts** so views can load CDN assets normally.
132
- - **Allows inline styles, eval, and inline scripts** for React + rsbuild's HMR.
133
+ - **Allows inline styles and inline scripts** for React.
133
134
 
134
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).
135
136
 
@@ -155,11 +156,22 @@ mount({
155
156
 
156
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.
157
158
 
158
- ### Data-at-rest in the temp directory
159
+ ### Data-at-rest
159
160
 
160
- 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.
161
162
 
162
- 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, keep it in memory in your CLI and inject it into the view via an authenticated `connect-src 'self'` fetch on boot rather than passing it through `data`.
163
+ For sensitive payloads use `dataLoader` instead of `data`:
164
+
165
+ ```ts
166
+ mount({
167
+ view: "report",
168
+ dataLoader: async () => {
169
+ return await db.fetchSensitiveRecords(); // stays in memory; never appears in HTML
170
+ },
171
+ });
172
+ ```
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. 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.
163
175
 
164
176
  ## Sharing views across users
165
177
 
@@ -279,7 +291,7 @@ Each pending mutation has a unique `id`. Multiple mutations can be in flight con
279
291
 
280
292
  ### Driving from Node via `mount()` directly
281
293
 
282
- 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):
283
295
 
284
296
  ```ts
285
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.3.3",
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"