Make the web dev server work from another machine (#665)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-15 10:29:07 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 48889b6550
commit a822cc93d4
7 changed files with 154 additions and 78 deletions
+37
View File
@@ -29,6 +29,23 @@ const iconsDir = normalizePath(
*/ */
const yaakTarget = process.env.YAAK_TARGET === "web" ? "web" : "desktop"; const yaakTarget = process.env.YAAK_TARGET === "web" ? "web" : "desktop";
/**
* Where `yaak-web` is listening, taken from the same variable that put it there.
*
* A wildcard bind is an instruction about what the server accepts, not an address
* to dial, so it becomes loopback here — the dev server and the send server share
* a machine.
*/
function sendServerUrl(): string {
const bind = process.env.YAAK_WEB_BIND?.trim();
if (!bind) return "http://127.0.0.1:9227";
const port = bind.slice(bind.lastIndexOf(":") + 1);
const host = bind.slice(0, bind.lastIndexOf(":"));
const dialable =
!host || host === "0.0.0.0" || host === "[::]" || host === "::" ? "127.0.0.1" : host;
return `http://${dialable}:${port}`;
}
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => { export default defineConfig(async () => {
return { return {
@@ -94,8 +111,28 @@ export default defineConfig(async () => {
}, },
clearScreen: false, clearScreen: false,
server: { server: {
// `HOST` names the interface, as it does most places: unset leaves Vite on
// loopback, `0.0.0.0` exposes it for reaching the dev server from another
// device.
host: process.env.HOST,
// Vite refuses a `Host` it does not recognise, which stops a page on a name the
// attacker controls from rebinding that name here and driving `/v1` as its own
// origin. Addresses are allowed already; only names need listing, so reaching
// this as `dev-box.example` means naming it. Deliberately not widened to "any
// host when exposed": the proxy below leads to an unauthenticated sender.
allowedHosts: process.env.ALLOWED_HOSTS?.split(",")
.map((h) => h.trim())
.filter(Boolean),
port: parseInt(process.env.YAAK_CLIENT_DEV_PORT ?? process.env.YAAK_DEV_PORT ?? "1420", 10), port: parseInt(process.env.YAAK_CLIENT_DEV_PORT ?? process.env.YAAK_DEV_PORT ?? "1420", 10),
strictPort: true, strictPort: true,
// A web dev server is one origin, the way the built app is: `/v1` is passed
// through to `yaak-web` rather than the tab being told to call it directly.
// That is one address to open instead of two, no CORS in the loop, and a
// dev build that sends exactly the way a production build does.
proxy:
yaakTarget === "web"
? { "/v1": { target: sendServerUrl(), changeOrigin: true } }
: undefined,
}, },
envPrefix: ["VITE_", "TAURI_"], envPrefix: ["VITE_", "TAURI_"],
}; };
+29 -23
View File
@@ -46,7 +46,7 @@ docker run -p 8080:8080 \
anything strangers can reach — see [What it refuses](#what-it-refuses-and-why). anything strangers can reach — see [What it refuses](#what-it-refuses-and-why).
Turn it on for an instance on your own network, where calling the API on the Turn it on for an instance on your own network, where calling the API on the
next machine is the whole point. Note that "private" is relative to the next machine is the whole point. Note that "private" is relative to the
*container*: `127.0.0.1` is the container itself, and reaching the Docker _container_: `127.0.0.1` is the container itself, and reaching the Docker
host means `host.docker.internal` (or `--network host`). host means `host.docker.internal` (or `--network host`).
- **`YAAK_WEB_RATE_LIMIT_PER_MINUTE`** defaults to 120 sends per client IP, - **`YAAK_WEB_RATE_LIMIT_PER_MINUTE`** defaults to 120 sends per client IP,
which suits a public instance and not a team of your own; `0` disables it. which suits a public instance and not a team of your own; `0` disables it.
@@ -72,27 +72,33 @@ cargo run -p yaak-web
YAAK_TARGET=web npm run dev --workspace @yaakapp/yaak-client YAAK_TARGET=web npm run dev --workspace @yaakapp/yaak-client
``` ```
A dev build looks for the server at `http://127.0.0.1:9227` (the Vite server is a The dev server passes `/v1` through to this binary, so a dev build sends to its
different origin and serves no `/v1`); a production build sends to its own own origin exactly like a production build does — one address to open, and no
origin unless `VITE_YAAK_WEB_URL` was set when it was built. CORS in the loop. `YAAK_WEB_BIND` moves this server and the dev server follows
it. A production build also sends to its own origin, unless `VITE_YAAK_WEB_URL`
was set when it was built.
Reaching the dev server from another machine is `HOST=0.0.0.0`. Vite allows
addresses but not names, so opening it as a hostname also needs
`ALLOWED_HOSTS=that-name`.
## Configuration ## Configuration
Every flag has a `YAAK_WEB_*` environment variable, so a container needs no Every flag has a `YAAK_WEB_*` environment variable, so a container needs no
arguments; `--help` lists them all. arguments; `--help` lists them all.
| Flag | Default | What | | Flag | Default | What |
| --- | --- | --- | | -------------------------- | ---------------- | --------------------------------------------------------------------------------------------- |
| `--serve` | off | Also serve a built web client from this directory, on the same origin. | | `--serve` | off | Also serve a built web client from this directory, on the same origin. |
| `--bind` | `127.0.0.1:9227` | Listen address. The image sets `0.0.0.0:8080`. | | `--bind` | `127.0.0.1:9227` | Listen address. The image sets `0.0.0.0:8080`. |
| `--allow-private-networks` | off | Allow sends to loopback, private and link-local addresses. | | `--allow-private-networks` | off | Allow sends to loopback, private and link-local addresses. |
| `--allowed-origins` | `*` | CORS origins, comma-separated. Unused when the app is served from here: same origin, no CORS. | | `--allowed-origins` | `*` | CORS origins, comma-separated. Unused when the app is served from here: same origin, no CORS. |
| `--max-request-bytes` | 16 MiB | Largest rendered request accepted from the tab. | | `--max-request-bytes` | 16 MiB | Largest rendered request accepted from the tab. |
| `--max-response-bytes` | 64 MiB | Largest upstream body relayed before the send is cut off. | | `--max-response-bytes` | 64 MiB | Largest upstream body relayed before the send is cut off. |
| `--max-timeout-secs` | 60 | Ceiling on a send's timeout; a request asking for more (or none) gets this. | | `--max-timeout-secs` | 60 | Ceiling on a send's timeout; a request asking for more (or none) gets this. |
| `--rate-limit-per-minute` | 120 | Sends per client IP per minute; 0 disables. | | `--rate-limit-per-minute` | 120 | Sends per client IP per minute; 0 disables. |
| `--max-concurrent` | 256 | Sends in flight at once. | | `--max-concurrent` | 256 | Sends in flight at once. |
| `--trust-forwarded-for` | off | Take the client IP from `X-Forwarded-For`. Only behind a load balancer that sets it. | | `--trust-forwarded-for` | off | Take the client IP from `X-Forwarded-For`. Only behind a load balancer that sets it. |
## Logging ## Logging
@@ -181,7 +187,7 @@ in `lib.rs`, or in middleware on the exported router. Put TLS in front of a publ
{ {
"request": { "url": "https://…", "method": "GET", "headers": [], "body": {}, "bodyType": null, "urlParameters": [] }, "request": { "url": "https://…", "method": "GET", "headers": [], "body": {}, "bodyType": null, "urlParameters": [] },
"settings": { "validateCertificates": true, "followRedirects": true, "timeoutMs": 0, "sendCookies": true, "storeCookies": true }, "settings": { "validateCertificates": true, "followRedirects": true, "timeoutMs": 0, "sendCookies": true, "storeCookies": true },
"cookies": [ ] "cookies": [ ]
} }
``` ```
@@ -193,13 +199,13 @@ jar's contents (or `null` for no jar).
The reply is `application/x-ndjson`, one JSON frame per line, in the order things The reply is `application/x-ndjson`, one JSON frame per line, in the order things
happened: happened:
| `type` | When | Carries | | `type` | When | Carries |
| --- | --- | --- | | ---------- | ----------------------------------------- | ---------------------------------------------------------------------------------- |
| `event` | as the engine produces them | one timeline event, in the desktop's `http_response_event.event` shape | | `event` | as the engine produces them | one timeline event, in the desktop's `http_response_event.event` shape |
| `response` | once, when the final hop's headers arrive | status, all headers, request headers as sent, remote address, HTTP version, timing | | `response` | once, when the final hop's headers arrive | status, all headers, request headers as sent, remote address, HTTP version, timing |
| `body` | as the body is read | a decompressed chunk, base64 | | `body` | as the body is read | a decompressed chunk, base64 |
| `done` | last, on success | elapsed, byte counts, and the cookie jar as the send left it | | `done` | last, on success | elapsed, byte counts, and the cookie jar as the send left it |
| `error` | last, on failure | the reason, and any cookies collected before the failure | | `error` | last, on failure | the reason, and any cookies collected before the failure |
Refusals that happen before anything is sent (a blocked destination, a bad body, Refusals that happen before anything is sent (a blocked destination, a bad body,
rate limit, capacity) are plain HTTP errors (`403`, `400`, `429`, `503`) with rate limit, capacity) are plain HTTP errors (`403`, `400`, `429`, `503`) with
+44 -43
View File
@@ -5,10 +5,11 @@ model layer — `yaak-models`, SQLite included — runs compiled to wasm inside
worker the tab talks to, so a browser stores exactly what a desktop install worker the tab talks to, so a browser stores exactly what a desktop install
stores, migrations and all. stores, migrations and all.
Select it at build time and run the frontend alone: Select it at build time. `vp run web:dev` does that and starts the send server
alongside the frontend:
```shell ```shell
YAAK_TARGET=web npm run dev --workspace @yaakapp/yaak-client vp run web:dev
``` ```
The flag resolves `@yaakapp-internal/platform` to `../index.web.ts`, which The flag resolves `@yaakapp-internal/platform` to `../index.web.ts`, which
@@ -30,17 +31,17 @@ tab (index.ts, commands.ts) ──MessagePort──▶ worker.ts ──▶ @yaak
◀── NDJSON: events, response, body, cookies ── ◀── NDJSON: events, response, body, cookies ──
``` ```
| File | What it is | | File | What it is |
| --- | --- | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `index.ts` | The `Platform` implementation. | | `index.ts` | The `Platform` implementation. |
| `commands.ts` | The command table: model commands forward to the worker; the rest is fixed answers and refusals-with-a-reason. | | `commands.ts` | The command table: model commands forward to the worker; the rest is fixed answers and refusals-with-a-reason. |
| `connection.ts` | A tab's end of the wire: request/response over a `MessagePort`, event delivery, and the tab's identity (`label`). | | `connection.ts` | A tab's end of the wire: request/response over a `MessagePort`, event delivery, and the tab's identity (`label`). |
| `send.ts` | Sending: the worker renders (`prepare_http_send`), the server executes, this file stores what comes back where the desktop stores it. | | `send.ts` | Sending: the worker renders (`prepare_http_send`), the server executes, this file stores what comes back where the desktop stores it. |
| `server.ts` | Where the Yaak server is, and the wire shapes it speaks (generated from `crates-server/yaak-web/src/wire.rs`). | | `server.ts` | Where the Yaak server is, and the wire shapes it speaks (generated from `crates-server/yaak-web/src/wire.rs`). |
| `worker.ts` | The process that owns the database. Loads the wasm, opens the DB once, answers each port, fans `model_writes` out to every port. | | `worker.ts` | The process that owns the database. Loads the wasm, opens the DB once, answers each port, fans `model_writes` out to every port. |
| `protocol.ts` | The message shapes both sides import. | | `protocol.ts` | The message shapes both sides import. |
| `errors.ts` | `UnsupportedCommandError`, the structured refusal. | | `errors.ts` | `UnsupportedCommandError`, the structured refusal. |
| `storage.ts` | `navigator.storage.persist()`. | | `storage.ts` | `navigator.storage.persist()`. |
The Rust side is `crates/yaak-wasm` (`@yaakapp-internal/wasm`): `boot()`, The Rust side is `crates/yaak-wasm` (`@yaakapp-internal/wasm`): `boot()`,
`rpc(cmd, payload, label)` returning `{ result, events }`, blob get/put, and `rpc(cmd, payload, label)` returning `{ result, events }`, blob get/put, and
@@ -70,7 +71,7 @@ Behaviours worth knowing before changing anything:
store's echo handling is unchanged. store's echo handling is unchanged.
- **Cascade rules, duplicate naming, id generation, serde defaults, and the - **Cascade rules, duplicate naming, id generation, serde defaults, and the
lazy first-run bootstrap are all the Rust code's.** Nothing about what a lazy first-run bootstrap are all the Rust code's.** Nothing about what a
model *is* is decided in TypeScript. model _is_ is decided in TypeScript.
- **Persistence is `relaxed-idb`**: SQLite pages live in IndexedDB, writes land - **Persistence is `relaxed-idb`**: SQLite pages live in IndexedDB, writes land
in memory and flush shortly after. A tab closing mid-flush loses at most the in memory and flush shortly after. A tab closing mid-flush loses at most the
last few writes. last few writes.
@@ -82,14 +83,14 @@ Behaviours worth knowing before changing anything:
### Implemented (32) ### Implemented (32)
| Group | Commands | | Group | Commands |
| --- | --- | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Models | `models_workspace_models`, `models_upsert`, `models_delete`, `models_duplicate`, `models_get_settings`, `models_get_graphql_introspection`, `models_upsert_graphql_introspection`, `models_grpc_events`, `models_websocket_events` | | Models | `models_workspace_models`, `models_upsert`, `models_delete`, `models_duplicate`, `models_get_settings`, `models_get_graphql_introspection`, `models_upsert_graphql_introspection`, `models_grpc_events`, `models_websocket_events` |
| Sending | `cmd_send_http_request` (through the Yaak server; see below) | | Sending | `cmd_send_http_request` (through the Yaak server; see below) |
| App | `cmd_metadata`, `cmd_get_workspace_meta`, `cmd_default_headers`, `cmd_get_themes`, `cmd_check_for_updates`, `cmd_dismiss_notification`, `cmd_plugin_init_errors` | | App | `cmd_metadata`, `cmd_get_workspace_meta`, `cmd_default_headers`, `cmd_get_themes`, `cmd_check_for_updates`, `cmd_dismiss_notification`, `cmd_plugin_init_errors` |
| Bodies | `cmd_http_response_body`, `cmd_http_response_body_path`, `cmd_http_request_body`, `cmd_get_http_response_events`, `cmd_get_sse_events` | | Bodies | `cmd_http_response_body`, `cmd_http_response_body_path`, `cmd_http_request_body`, `cmd_get_http_response_events`, `cmd_get_sse_events` |
| Plugin surfaces (empty results) | `cmd_http_request_actions`, `cmd_websocket_request_actions`, `cmd_grpc_request_actions`, `cmd_workspace_actions`, `cmd_folder_actions`, `cmd_template_function_summaries`, `cmd_get_http_authentication_summaries`, `cmd_get_http_authentication_config` | | Plugin surfaces (empty results) | `cmd_http_request_actions`, `cmd_websocket_request_actions`, `cmd_grpc_request_actions`, `cmd_workspace_actions`, `cmd_folder_actions`, `cmd_template_function_summaries`, `cmd_get_http_authentication_summaries`, `cmd_get_http_authentication_config` |
| Text | `cmd_format_json`, `cmd_render_template` | | Text | `cmd_format_json`, `cmd_render_template` |
Some of these answer honestly rather than fully, and the difference matters: Some of these answer honestly rather than fully, and the difference matters:
@@ -112,15 +113,15 @@ Each returns an `UnsupportedCommandError` carrying `cmd`, a user-facing
`message`, and the `capability` a caller should have checked. The UI turns it `message`, and the `capability` a caller should have checked. The UI turns it
into a toast. into a toast.
| Reason | Commands | | Reason | Commands |
| --- | --- | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sending, the parts not wired yet | `cmd_send_ephemeral_request`, `cmd_delete_send_history`, `cmd_delete_all_http_responses`, `cmd_import_url` | | Sending, the parts not wired yet | `cmd_send_ephemeral_request`, `cmd_delete_send_history`, `cmd_delete_all_http_responses`, `cmd_import_url` |
| No plugin runtime | `cmd_reload_plugins`, `cmd_plugin_info`, `cmd_plugins_search`, `cmd_plugins_install`, `cmd_plugins_install_from_directory`, `cmd_plugins_uninstall`, `cmd_plugins_updates`, `cmd_plugins_update_all`, `cmd_template_function_config`, `cmd_template_tokens_to_string`, `cmd_call_http_request_action`, `cmd_call_websocket_request_action`, `cmd_call_grpc_request_action`, `cmd_call_workspace_action`, `cmd_call_folder_action`, `cmd_call_http_authentication_action`, `cmd_curl_to_request`, `cmd_format_graphql` | | No plugin runtime | `cmd_reload_plugins`, `cmd_plugin_info`, `cmd_plugins_search`, `cmd_plugins_install`, `cmd_plugins_install_from_directory`, `cmd_plugins_uninstall`, `cmd_plugins_updates`, `cmd_plugins_update_all`, `cmd_template_function_config`, `cmd_template_tokens_to_string`, `cmd_call_http_request_action`, `cmd_call_websocket_request_action`, `cmd_call_grpc_request_action`, `cmd_call_workspace_action`, `cmd_call_folder_action`, `cmd_call_http_authentication_action`, `cmd_curl_to_request`, `cmd_format_graphql` |
| No filesystem | `cmd_import_data`, `cmd_export_data`, `cmd_save_response`, `cmd_save_base64_to_binary` | | No filesystem | `cmd_import_data`, `cmd_export_data`, `cmd_save_response`, `cmd_save_base64_to_binary` |
| Needs a real socket | `cmd_grpc_reflect`, `cmd_grpc_go`, `cmd_delete_all_grpc_connections`, `cmd_ws_connect`, `cmd_ws_send`, `cmd_ws_close`, `cmd_ws_delete_connections` | | Needs a real socket | `cmd_grpc_reflect`, `cmd_grpc_go`, `cmd_delete_all_grpc_connections`, `cmd_ws_connect`, `cmd_ws_send`, `cmd_ws_close`, `cmd_ws_delete_connections` |
| Workspace encryption | `cmd_enable_encryption`, `cmd_disable_encryption`, `cmd_reveal_workspace_key`, `cmd_set_workspace_key`, `cmd_secure_template`, `cmd_decrypt_template` | | Workspace encryption | `cmd_enable_encryption`, `cmd_disable_encryption`, `cmd_reveal_workspace_key`, `cmd_set_workspace_key`, `cmd_secure_template`, `cmd_decrypt_template` |
| One tab, no windows | `cmd_new_child_window`, `cmd_new_main_window`, `cmd_restart` | | One tab, no windows | `cmd_new_child_window`, `cmd_new_main_window`, `cmd_restart` |
| Other | `cmd_send_feedback` | | Other | `cmd_send_feedback` |
### Refused generically (34) ### Refused generically (34)
@@ -136,14 +137,14 @@ should be able to see which.
Reported honestly, so callers gate on the question rather than on the host: Reported honestly, so callers gate on the question rather than on the host:
| True | False | | True | False |
| --- | --- | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `httpSending`, `timeline`, `cookieJar` | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `multiWindow`, `windowChrome`, `interfaceZoom`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` | | `httpSending`, `timeline`, `cookieJar` | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `multiWindow`, `windowChrome`, `interfaceZoom`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` |
`interfaceZoom: false` leaves Cmd/Ctrl `+`, `-` and `0` to the browser instead `interfaceZoom: false` leaves Cmd/Ctrl `+`, `-` and `0` to the browser instead
of swallowing them, and drops those three rows from the hotkeys screen. of swallowing them, and drops those three rows from the hotkeys screen.
`multiWindow: false` means the host cannot open a *second window* on demand — `multiWindow: false` means the host cannot open a _second window_ on demand —
what `cmd_new_child_window` does for Settings and workspace switching. It is not what `cmd_new_child_window` does for Settings and workspace switching. It is not
a claim that nothing else is looking: other tabs may well be open on the same a claim that nothing else is looking: other tabs may well be open on the same
worker, and it pushes every write to all of them regardless. worker, and it pushes every write to all of them regardless.
@@ -159,7 +160,7 @@ for the desktop's window label. The worker fans each write out to every
connected tab, and the receiving tab's store applies or ignores it exactly as a connected tab, and the receiving tab's store applies or ignores it exactly as a
desktop window would. desktop window would.
The label is deliberately *not* kept in `sessionStorage`: duplicating a tab The label is deliberately _not_ kept in `sessionStorage`: duplicating a tab
copies session storage, and two tabs sharing one identity would each mistake the copies session storage, and two tabs sharing one identity would each mistake the
other's writes for an echo of their own and drop them. other's writes for an echo of their own and drop them.
@@ -207,7 +208,7 @@ network half of a send runs on a small stateless server,
**What sends today:** any saved request whose templates are variables and whose **What sends today:** any saved request whose templates are variables and whose
authentication is none, or an inline header. Sending a request that needs a authentication is none, or an inline header. Sending a request that needs a
template *function* (`${[ timestamp() ]}`) or an authentication plugin (bearer, template _function_ (`${[ timestamp() ]}`) or an authentication plugin (bearer,
basic, OAuth, …) is refused before anything leaves the tab, with a message naming basic, OAuth, …) is refused before anything leaves the tab, with a message naming
what it needs; those light up when plugins run in the browser. Requests with a what it needs; those light up when plugins run in the browser. Requests with a
file body or multipart file fields are refused by the server (it has no access to file body or multipart file fields are refused by the server (it has no access to
@@ -217,11 +218,11 @@ private ranges outright — that is what the desktop app is for. A self-hosted
server on your own network can be started with `--allow-private-networks`, which server on your own network can be started with `--allow-private-networks`, which
is the one case where those addresses are the user's to reach. is the one case where those addresses are the user's to reach.
**Where the tab sends** (`server.ts`): a production build posts to `/v1/http/send` **Where the tab sends** (`server.ts`): its own origin, in development as well as
on its own origin, because the server can serve the app itself production, so there is no address to learn and no CORS in the loop. A
(`yaak-web --serve dist/apps/yaak-client`, which is what the production build is served by the sender itself (`yaak-web --serve
`ghcr.io/mountain-loop/yaak-web` image runs) — same origin, so no CORS and dist/apps/yaak-client`, which is what the `ghcr.io/mountain-loop/yaak-web`
nothing to configure. A dev build falls back to `http://127.0.0.1:9227`, since image runs). In development the Vite server passes `/v1` through to `yaak-web`
the Vite server is a different origin and serves no `/v1`; run one with instead, following `YAAK_WEB_BIND` to find it — `vp run web:dev` starts both.
`cargo run -p yaak-web`. `VITE_YAAK_WEB_URL` overrides both, for a `VITE_YAAK_WEB_URL` overrides this, for a deployment that keeps the app and the
deployment that keeps the app and the server apart. server apart.
+2 -1
View File
@@ -11,6 +11,7 @@
*/ */
import type { Unsubscribe } from "../types"; import type { Unsubscribe } from "../types";
import { randomId } from "./ids";
import { type FromWorker, type ToWorker, WORKER_NAME } from "./protocol"; import { type FromWorker, type ToWorker, WORKER_NAME } from "./protocol";
/** /**
@@ -45,7 +46,7 @@ export class WorkerConnection {
* and two tabs claiming one identity would each drop the other's writes as * and two tabs claiming one identity would each drop the other's writes as
* echoes. * echoes.
*/ */
readonly label = `tab_${crypto.randomUUID().slice(0, 8)}`; readonly label = `tab_${randomId(4)}`;
/** True once the worker has said anything at all. */ /** True once the worker has said anything at all. */
private heard = false; private heard = false;
+14
View File
@@ -0,0 +1,14 @@
/**
* Random ids that do not need a secure context.
*
* `crypto.randomUUID` exists only on HTTPS and localhost, so a tab opened at
* `http://some-host:1424` — a self-hosted instance reached from another machine —
* throws on it before anything renders. `crypto.getRandomValues` carries no such
* restriction, and nothing here needs UUID semantics: these ids identify a tab and
* a stream within one page, and only have to not collide with each other.
*/
export function randomId(bytes = 8): string {
const buf = new Uint8Array(bytes);
crypto.getRandomValues(buf);
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
}
+22 -5
View File
@@ -28,6 +28,7 @@ import type {
import { commandSupport, runCommand } from "./commands"; import { commandSupport, runCommand } from "./commands";
import { WorkerConnection } from "./connection"; import { WorkerConnection } from "./connection";
import { unsupported } from "./errors"; import { unsupported } from "./errors";
import { randomId } from "./ids";
import { serverBaseUrl } from "./server"; import { serverBaseUrl } from "./server";
import { requestPersistence } from "./storage"; import { requestPersistence } from "./storage";
@@ -184,10 +185,18 @@ export function createWebPlatform(): Platform {
clipboard: { clipboard: {
writeText: (text) => navigator.clipboard.writeText(text), writeText: (text) => navigator.clipboard.writeText(text),
readText: async () => { readText: async () => {
throw unsupported("clipboard.readText", "Paste instead — Yaak in a browser can't read the clipboard on its own", "clipboardRead"); throw unsupported(
"clipboard.readText",
"Paste instead — Yaak in a browser can't read the clipboard on its own",
"clipboardRead",
);
}, },
clear: async () => { clear: async () => {
throw unsupported("clipboard.clear", "Yaak in a browser can't modify the clipboard", "clipboardRead"); throw unsupported(
"clipboard.clear",
"Yaak in a browser can't modify the clipboard",
"clipboardRead",
);
}, },
}, },
@@ -200,7 +209,11 @@ export function createWebPlatform(): Platform {
files: { files: {
readDir: async () => { readDir: async () => {
throw unsupported("files.readDir", "A browser tab can't browse your filesystem", "localFiles"); throw unsupported(
"files.readDir",
"A browser tab can't browse your filesystem",
"localFiles",
);
}, },
readText: async () => { readText: async () => {
throw unsupported("files.readText", "A browser tab can't read local files", "localFiles"); throw unsupported("files.readText", "A browser tab can't read local files", "localFiles");
@@ -243,7 +256,7 @@ export function createWebPlatform(): Platform {
): Promise<RpcStreamHandle<T>> { ): Promise<RpcStreamHandle<T>> {
// Same shape as the desktop — subscribe first, then dispatch — so that a // Same shape as the desktop — subscribe first, then dispatch — so that a
// command which grows the ability to stream here needs no caller changes. // command which grows the ability to stream here needs no caller changes.
const streamId = crypto.randomUUID(); const streamId = randomId();
const unlisten = db.listen(`stream_${streamId}`, (p) => onMessage(p as M)); const unlisten = db.listen(`stream_${streamId}`, (p) => onMessage(p as M));
try { try {
const result = (await runCommand(cmd, { ...payload, streamId }, db)) as T; const result = (await runCommand(cmd, { ...payload, streamId }, db)) as T;
@@ -268,7 +281,11 @@ export function createWebPlatform(): Platform {
}, },
revealItemInDir: async () => { revealItemInDir: async () => {
throw unsupported("revealItemInDir", "A browser tab can't open your file manager", "localFiles"); throw unsupported(
"revealItemInDir",
"A browser tab can't open your file manager",
"localFiles",
);
}, },
osType: detectOsType, osType: detectOsType,
+6 -6
View File
@@ -16,16 +16,16 @@ import type { Frame } from "@yaakapp-internal/web";
* (`yaak-web --serve`), and then a send is a request to a path on the * (`yaak-web --serve`), and then a send is a request to a path on the
* page's own origin — no CORS, and nothing for a self-hoster to configure. * page's own origin — no CORS, and nothing for a self-hoster to configure.
* *
* `VITE_YAAK_WEB_URL` overrides it at build time, for a deployment that * `VITE_YAAK_WEB_URL` overrides it at build time, for a deployment that keeps the
* keeps the two apart. The dev server is one of those: it serves the app on its * two apart. Nothing else needs to: the dev server passes `/v1` through to
* own origin and knows nothing about `/v1`, so a dev build falls back to a server * `yaak-web` (see the client's vite config), so a dev build and a production
* running locally (`cargo run -p yaak-web`). * build are both talking to their own origin, and neither has an address to
* learn. A tab opened from another machine therefore works unchanged.
*/ */
export function serverBaseUrl(): string { export function serverBaseUrl(): string {
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env; const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env;
const configured = env?.VITE_YAAK_WEB_URL?.trim(); const configured = env?.VITE_YAAK_WEB_URL?.trim();
if (configured) return configured.replace(/\/+$/, ""); return configured ? configured.replace(/\/+$/, "") : "";
return env?.DEV ? "http://127.0.0.1:9227" : "";
} }
export function serverSendUrl(): string { export function serverSendUrl(): string {